From d8d9407422d305cc1f8f3b8bc57d8b2212bd2906 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:06:37 +0300 Subject: [PATCH 01/26] Turning off posts use-case (unused) --- .../post/create-old/create-post.interactor.ts | 0 .../post/create-old/create.interactor.ts | 1 - ....request.ts => delete-post.request.ts.off} | 0 ....usecase.ts => delete-post.usecase.ts.off} | 0 .../post/delete/{index.ts => index.ts.off} | 0 ...st.request.ts => edit-post.request.ts.off} | 0 ...st.usecase.ts => edit-post.usecase.ts.off} | 0 .../post/edit/{index.ts => index.ts.off} | 0 ...equest.ts => find-all-post.request.ts.off} | 0 ...secase.ts => find-all-post.usecase.ts.off} | 0 .../post/find-all/{index.ts => index.ts.off} | 0 ...uest.ts => find-by-id-post.request.ts.off} | 0 ...case.ts => find-by-id-post.usecase.ts.off} | 0 .../find-by-id/{index.ts => index.ts.off} | 0 .../web/rest/controllers/post.controller.ts | 34 ------------------- 15 files changed, 35 deletions(-) delete mode 100644 server/src/application/post/create-old/create-post.interactor.ts delete mode 100644 server/src/application/post/create-old/create.interactor.ts rename server/src/application/post/delete/{delete-post.request.ts => delete-post.request.ts.off} (100%) rename server/src/application/post/delete/{delete-post.usecase.ts => delete-post.usecase.ts.off} (100%) rename server/src/application/post/delete/{index.ts => index.ts.off} (100%) rename server/src/application/post/edit/{edit-post.request.ts => edit-post.request.ts.off} (100%) rename server/src/application/post/edit/{edit-post.usecase.ts => edit-post.usecase.ts.off} (100%) rename server/src/application/post/edit/{index.ts => index.ts.off} (100%) rename server/src/application/post/find-all/{find-all-post.request.ts => find-all-post.request.ts.off} (100%) rename server/src/application/post/find-all/{find-all-post.usecase.ts => find-all-post.usecase.ts.off} (100%) rename server/src/application/post/find-all/{index.ts => index.ts.off} (100%) rename server/src/application/post/find-by-id/{find-by-id-post.request.ts => find-by-id-post.request.ts.off} (100%) rename server/src/application/post/find-by-id/{find-by-id-post.usecase.ts => find-by-id-post.usecase.ts.off} (100%) rename server/src/application/post/find-by-id/{index.ts => index.ts.off} (100%) delete mode 100644 server/src/web/rest/controllers/post.controller.ts diff --git a/server/src/application/post/create-old/create-post.interactor.ts b/server/src/application/post/create-old/create-post.interactor.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/server/src/application/post/create-old/create.interactor.ts b/server/src/application/post/create-old/create.interactor.ts deleted file mode 100644 index 8b137891..00000000 --- a/server/src/application/post/create-old/create.interactor.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/server/src/application/post/delete/delete-post.request.ts b/server/src/application/post/delete/delete-post.request.ts.off similarity index 100% rename from server/src/application/post/delete/delete-post.request.ts rename to server/src/application/post/delete/delete-post.request.ts.off diff --git a/server/src/application/post/delete/delete-post.usecase.ts b/server/src/application/post/delete/delete-post.usecase.ts.off similarity index 100% rename from server/src/application/post/delete/delete-post.usecase.ts rename to server/src/application/post/delete/delete-post.usecase.ts.off diff --git a/server/src/application/post/delete/index.ts b/server/src/application/post/delete/index.ts.off similarity index 100% rename from server/src/application/post/delete/index.ts rename to server/src/application/post/delete/index.ts.off diff --git a/server/src/application/post/edit/edit-post.request.ts b/server/src/application/post/edit/edit-post.request.ts.off similarity index 100% rename from server/src/application/post/edit/edit-post.request.ts rename to server/src/application/post/edit/edit-post.request.ts.off diff --git a/server/src/application/post/edit/edit-post.usecase.ts b/server/src/application/post/edit/edit-post.usecase.ts.off similarity index 100% rename from server/src/application/post/edit/edit-post.usecase.ts rename to server/src/application/post/edit/edit-post.usecase.ts.off diff --git a/server/src/application/post/edit/index.ts b/server/src/application/post/edit/index.ts.off similarity index 100% rename from server/src/application/post/edit/index.ts rename to server/src/application/post/edit/index.ts.off diff --git a/server/src/application/post/find-all/find-all-post.request.ts b/server/src/application/post/find-all/find-all-post.request.ts.off similarity index 100% rename from server/src/application/post/find-all/find-all-post.request.ts rename to server/src/application/post/find-all/find-all-post.request.ts.off diff --git a/server/src/application/post/find-all/find-all-post.usecase.ts b/server/src/application/post/find-all/find-all-post.usecase.ts.off similarity index 100% rename from server/src/application/post/find-all/find-all-post.usecase.ts rename to server/src/application/post/find-all/find-all-post.usecase.ts.off diff --git a/server/src/application/post/find-all/index.ts b/server/src/application/post/find-all/index.ts.off similarity index 100% rename from server/src/application/post/find-all/index.ts rename to server/src/application/post/find-all/index.ts.off diff --git a/server/src/application/post/find-by-id/find-by-id-post.request.ts b/server/src/application/post/find-by-id/find-by-id-post.request.ts.off similarity index 100% rename from server/src/application/post/find-by-id/find-by-id-post.request.ts rename to server/src/application/post/find-by-id/find-by-id-post.request.ts.off diff --git a/server/src/application/post/find-by-id/find-by-id-post.usecase.ts b/server/src/application/post/find-by-id/find-by-id-post.usecase.ts.off similarity index 100% rename from server/src/application/post/find-by-id/find-by-id-post.usecase.ts rename to server/src/application/post/find-by-id/find-by-id-post.usecase.ts.off diff --git a/server/src/application/post/find-by-id/index.ts b/server/src/application/post/find-by-id/index.ts.off similarity index 100% rename from server/src/application/post/find-by-id/index.ts rename to server/src/application/post/find-by-id/index.ts.off diff --git a/server/src/web/rest/controllers/post.controller.ts b/server/src/web/rest/controllers/post.controller.ts deleted file mode 100644 index 6479566c..00000000 --- a/server/src/web/rest/controllers/post.controller.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CreatePostCommandFf } from '@application/posts/create'; -import { CommandBus } from '@nestjs/cqrs'; -import { ExpressHandler } from '../infrastucture/express-handler'; -import { CreatePostRequestDto } from '../dtos/requests'; -import { CreatePostResponseDto } from '../dtos/responses'; - -export class PostController { - constructor(private commandBus: CommandBus) {} - - // @Post() - // @UseGuards(JwtAuthGuard) - // async createPost(@Body() createPostDto: CreatePostDto, @Request() req) { - // const userId = req.user.userId; - // return this.postService.createPost(createPostDto, userId); - // } - - createPost: ExpressHandler = - async (req, res) => { - const { title, authorId, content } = req.body; - - if (!title || !authorId || !content) { - return res.json(); - } - - let result = this.commandBus.execute( - new CreatePostCommandFf(title, content, authorId), - ); - - res.status(201).json({ - success : true, - - }); - }; -} From 37327a577dadf9442c5a725ba9ef7c146ddad249 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:07:17 +0300 Subject: [PATCH 02/26] Adding findById to user repository implementation --- server/src/domain/repositories/user.repository.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/domain/repositories/user.repository.ts b/server/src/domain/repositories/user.repository.ts index a8f1d0f5..2fcc5b2f 100644 --- a/server/src/domain/repositories/user.repository.ts +++ b/server/src/domain/repositories/user.repository.ts @@ -1,4 +1,5 @@ import { User } from '@domain/entities'; +import { Nullable } from '@domain/shared/types'; export interface IUserRepository { saveUser(user: User): Promise; @@ -10,4 +11,5 @@ export interface IUserRepository { isEmailExists(payload: string): Promise; isUsernameExists(payload: string): Promise; updateUser(user: User): Promise; + findById(id: string): Promise>; } From 10a125fc533cb41865912c01b162d8371f10c2b3 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:08:36 +0300 Subject: [PATCH 03/26] Modifying the payload type to issue a jwt --- server/src/application/auth/register/register.use-case.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/application/auth/register/register.use-case.ts b/server/src/application/auth/register/register.use-case.ts index 2db0a0c7..64301846 100644 --- a/server/src/application/auth/register/register.use-case.ts +++ b/server/src/application/auth/register/register.use-case.ts @@ -26,9 +26,9 @@ class RegisterUsecase extends BaseUseCase { const createdUser = await this._userRepository.saveUser(user); log('createdUser', createdUser); const result: AuthResponseDto = { - token: this._jwtService.sign(createdUser.id!), + token: this._jwtService.sign({ userId: createdUser.id as string }), tokenExpiration: new Date(Date.now() + 1000 * 60 * 60), - refreshToken: this._jwtService.sign(createdUser.id!), + refreshToken: this._jwtService.sign({ userId: createdUser.id as string }), refreshTokenExpiration: new Date(Date.now() + 1000 * 60 * 60 * 24), userDetails: createdUser, }; From 59706e1f40d761c3398713f4e6b08eb39bb38f8f Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:11:58 +0300 Subject: [PATCH 04/26] Modifying JWT payload type and better exception types --- .../src/application/auth/login/login.use-case.ts | 15 +++++++++++---- server/src/contracts/services/IJwt.ts | 8 ++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/server/src/application/auth/login/login.use-case.ts b/server/src/application/auth/login/login.use-case.ts index 41371a17..001a4694 100644 --- a/server/src/application/auth/login/login.use-case.ts +++ b/server/src/application/auth/login/login.use-case.ts @@ -1,11 +1,12 @@ import { AuthRequestDto, AuthResponseDto } from '@contracts/dtos/auth'; import { UnauthorizedError } from '@contracts/errors/unauthorized.error'; import { IHasherService } from '@contracts/services/IHasher'; -import { IJwtService } from '@contracts/services/IJwt'; +import { IJwtService, JwtPayload } from '@contracts/services/IJwt'; import { TYPES } from '@infrastructure/shared/ioc/types'; import { inject, injectable } from 'inversify'; import { BaseUseCase } from '@application/shared'; import { IUserRepository } from '@domain/repositories/user.repository'; +import { log } from 'console'; @injectable() class LoginUseCase extends BaseUseCase { constructor( @@ -15,7 +16,10 @@ class LoginUseCase extends BaseUseCase { ) { super(); } - public async performOperation(request: AuthRequestDto): Promise { + public async performOperation( + request: AuthRequestDto, + ): Promise { + log('request login use case', request); const user = (await this._userRepository.findByEmail(request.login)) || (await this._userRepository.findByUsername(request.login)); @@ -25,11 +29,14 @@ class LoginUseCase extends BaseUseCase { ) { throw new UnauthorizedError('Invalid credentials'); } - const token = this._jwtService.sign(user.id!); + if (!user) { + throw new UnauthorizedError('User not found'); + } + const token = this._jwtService.sign({ userId: user.id as string }); const result: AuthResponseDto = { token, tokenExpiration: new Date(Date.now() + 1000 * 60 * 60), - refreshToken: this._jwtService.sign(user.id!), + refreshToken: this._jwtService.sign({ userId: user.id as string }), refreshTokenExpiration: new Date(Date.now() + 1000 * 60 * 60 * 24), userDetails: user, }; diff --git a/server/src/contracts/services/IJwt.ts b/server/src/contracts/services/IJwt.ts index 148aa99a..06858028 100644 --- a/server/src/contracts/services/IJwt.ts +++ b/server/src/contracts/services/IJwt.ts @@ -1,6 +1,10 @@ interface IJwtService { - sign: (payload: string) => string; - verify: (token: string) => string; + sign: (payload: JwtPayload) => string; + verify: (token: string) => JwtPayload; +} + +export interface JwtPayload { + userId: string; } export { IJwtService }; From cce60f505b6f89d3214ff01f471738c4881bfd9f Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:14:25 +0300 Subject: [PATCH 05/26] Mapping from Post entity to PostDetailsResponseDto --- .../contracts/dtos/posts/post-details.response.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/contracts/dtos/posts/post-details.response.ts b/server/src/contracts/dtos/posts/post-details.response.ts index 4a2c2f4b..08175ba4 100644 --- a/server/src/contracts/dtos/posts/post-details.response.ts +++ b/server/src/contracts/dtos/posts/post-details.response.ts @@ -1,3 +1,4 @@ +import { Post } from '@domain/entities'; import { Nullable } from '@domain/shared/types'; import { UserResponseDto } from '@dtos/users'; @@ -9,5 +10,16 @@ export class PostDetailsResponseDto { public author: UserResponseDto, public createdAt: Date, public lastModifiedAt: Nullable, - ) { } + ) {} + + public static fromEntity(entity: Post): PostDetailsResponseDto { + return new PostDetailsResponseDto( + entity.id ?? '', + entity.title, + entity.content, + UserResponseDto.fromEntity(entity.author), + entity.createdAt, + entity.updatedAt, + ); + } } From d53c4c3df9d1dee9ba78ec6f60f19102f4010bdc Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:17:01 +0300 Subject: [PATCH 06/26] Adding create post use-case --- .../post/create/create-post.usecase.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/server/src/application/post/create/create-post.usecase.ts b/server/src/application/post/create/create-post.usecase.ts index 2d513675..6cce5c2b 100644 --- a/server/src/application/post/create/create-post.usecase.ts +++ b/server/src/application/post/create/create-post.usecase.ts @@ -1,20 +1,32 @@ import { BaseUseCase, UseCase } from '@application/shared'; import { CreatePostRequest } from './create-post.request'; import { PostDetailsResponseDto } from '@contracts/dtos/posts'; +import { inject, injectable } from 'inversify'; +import { TYPES } from '@infrastructure/shared/ioc/types'; +import { IPostRepository } from '@domain/repositories/post.repository'; +import { LOGGER } from '@/web/rest/logger'; -@UseCase() +@injectable() class CreatePostUseCase extends BaseUseCase< CreatePostRequest, PostDetailsResponseDto > { - constructor() { + constructor( + @inject(TYPES.IPostRepository) private _postRepository: IPostRepository, + ) { super(); } - public async performOperation({}: CreatePostRequest): Promise { - throw new Error('Method not implemented.'); + public async performOperation( + request: CreatePostRequest, + ): Promise { + const post = CreatePostRequest.toEntity(request); + const createdPost = await this._postRepository.createPost(post); + if (createdPost) { + return PostDetailsResponseDto.fromEntity(createdPost); + } + return {} as PostDetailsResponseDto; } } export { CreatePostUseCase }; - From ccffef4f12903b8d5c7607fd69fd6f28da5c5a11 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:18:49 +0300 Subject: [PATCH 07/26] Modifying CreatePostRequest object (Uploaded files are not a optional arg) --- .../post/create/create-post.request.ts | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/server/src/application/post/create/create-post.request.ts b/server/src/application/post/create/create-post.request.ts index 691d877d..6fa7557b 100644 --- a/server/src/application/post/create/create-post.request.ts +++ b/server/src/application/post/create/create-post.request.ts @@ -1,37 +1,79 @@ import { UseCaseRequest } from '@application/shared'; +import { Post, User } from '@domain/entities'; +import { POST_STATUS } from '@domain/eums/post-status.enum'; import { TriggeredBy } from '@domain/shared/entities/triggered-by'; import { UploadedFiles } from '@domain/shared/models'; -// import { InvalidParameterException } from '@domain/shared/exceptions'; +import { InvalidParameterException } from '@domain/shared/exceptions'; class CreatePostRequest extends UseCaseRequest { readonly title: string; readonly content: string; + readonly authorId: string; + readonly author: User; readonly attachments: UploadedFiles[]; + readonly status: POST_STATUS; // Constructor Section constructor( triggeredBy: TriggeredBy, title: string, content: string, - attachments?: UploadedFiles[], + authorId: string, + author: User, + attachments: UploadedFiles[], + status: POST_STATUS = POST_STATUS.DRAFT, ) { super(triggeredBy); this.title = title; this.content = content; - this.attachments = attachments || []; + this.authorId = authorId; + this.author = author; + this.attachments = attachments; + this.status = status; + } + + public static toEntity(request: CreatePostRequest): Post { + return Post.create( + null, + request.title, + request.content, + request.triggeredBy.who, + request.author, + [], + [], + request.status, + new Date(), + null, + null, + ); } public static create( triggeredBy: TriggeredBy, title: string, content: string, + authorId: string, + author: User, attachments: UploadedFiles[], + status: POST_STATUS = POST_STATUS.DRAFT, ): CreatePostRequest { - return new CreatePostRequest(triggeredBy, title, content, attachments); + return new CreatePostRequest( + triggeredBy, + title, + content, + authorId, + author, + attachments, + status, + ); } // Validate here using EnsureClass - protected validatePayload(): void { } + protected validatePayload(): void { + if (!this.title || !this.content || !this.authorId || !this.author) { + throw new InvalidParameterException('Invalid request'); + } + } } export { CreatePostRequest }; From 711f10db893e4149cdafed0db23b2f62cabaafca Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:19:23 +0300 Subject: [PATCH 08/26] Adding post dependencies in the IOC container --- server/src/infrastructure/shared/ioc/inversify.config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/infrastructure/shared/ioc/inversify.config.ts b/server/src/infrastructure/shared/ioc/inversify.config.ts index 30f96207..8fadea2b 100644 --- a/server/src/infrastructure/shared/ioc/inversify.config.ts +++ b/server/src/infrastructure/shared/ioc/inversify.config.ts @@ -11,6 +11,9 @@ import { LoginUseCase } from '@application/auth/login/login.use-case'; import { RegisterUsecase } from '@application/auth/register/register.use-case'; import { DataSource } from 'typeorm'; import { appDataSource } from '@infrastructure/shared/persistence/data-source'; +import { PostsController } from '@/web/rest/controllers/posts.controller'; +import { PostRepository } from '@infrastructure/posts'; +import { CreatePostUseCase } from '@application/post'; const container = new Container(); // Bind the extrernal dependencies @@ -20,13 +23,17 @@ container.bind(TYPES.IUserMapper).to(UserMapper); // Inject the repositories container.bind(TYPES.IUserRepository).to(UserRepository); +container.bind(TYPES.IPostRepository).to(PostRepository); // Inject input ports container.bind(TYPES.ILoginInputPort).to(LoginUseCase); container.bind(TYPES.IRegisterInputPort).to(RegisterUsecase); +container.bind(TYPES.ICreatePostInputPort).to(CreatePostUseCase); + // Bind the controllers container.bind(TYPES.IAuthController).to(AuthController); +container.bind(TYPES.IPostController).to(PostsController); // Bind ApplicationRouter container.bind(ApplicationRouter).to(ApplicationRouter); From 54aea1204afb43ce4e7ad846d4c81eb0262c20c3 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:20:37 +0300 Subject: [PATCH 09/26] Adding posts repository implementation --- .../posts/post.repository.impl.ts | 102 ++++++++---------- 1 file changed, 44 insertions(+), 58 deletions(-) diff --git a/server/src/infrastructure/posts/post.repository.impl.ts b/server/src/infrastructure/posts/post.repository.impl.ts index 600e1084..27d61972 100644 --- a/server/src/infrastructure/posts/post.repository.impl.ts +++ b/server/src/infrastructure/posts/post.repository.impl.ts @@ -9,86 +9,72 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity import { injectable } from 'inversify'; import { Post } from '@domain/entities'; import { IPostRepository } from '@domain/repositories/post.repository'; +import { PostPersistence } from './post.persistence'; +import { PostMapper } from './post.mapper'; @injectable() -export class PostRepositoryImpl - extends Repository - implements IPostRepository -{ - constructor(dataSource: DataSource) { - super(Post, dataSource.createEntityManager()); +export class PostRepository implements IPostRepository { + private _repository: Repository; + + constructor(private readonly dataSource: DataSource) { + this._repository = this.dataSource.getRepository(PostPersistence); + } + async findByAuthor(authorId: string): Promise { + const post = await this._repository.findOne({ where: { authorId } }); + return post ? PostMapper.toDomain(post) : null; } async createPost(post: Post): Promise { - return this.save(post); + const postPersistence = PostMapper.toPersistence(post); + const createdPost = await this._repository.save(postPersistence); + return PostMapper.toDomain(createdPost); } async findPostById(postId: string): Promise { - return this.createQueryBuilder('post') - .where('post.id = :postId', { postId }) - .getOne(); + const post = await this._repository.findOne({ where: { id: postId } }); + return post ? PostMapper.toDomain(post) : null; } - async findUserPosts(limit: number, page: number, userId: string) { - return this.createQueryBuilder('post') - .where('post.authorId = :userId', { userId }) - .orWhere('post.editors = :userId', { userId }) - .take(limit || 10) - .skip(page * limit) - .getMany(); + async findUserPosts( + limit: number, + page: number, + userId: string, + ): Promise { + const [posts, count] = await this._repository.findAndCount({ + where: { authorId: userId }, + take: limit, + skip: (page - 1) * limit, + }); + return posts.map(PostMapper.toDomain); } - async findAll(limit: number, page: number) { - return this.createQueryBuilder('post') - .take(limit || 10) - .skip(page * limit) - .getMany(); + async findAll(limit: number, page: number): Promise { + const [posts, count] = await this._repository.findAndCount({ + take: limit, + skip: (page - 1) * limit, + }); + return posts.map(PostMapper.toDomain); } async findByTitle(title: string): Promise { - return this.createQueryBuilder('post') - .where('post.title = :title', { title }) - .getOne(); + const post = await this._repository.findOne({ where: { title } }); + return post ? PostMapper.toDomain(post) : null; } async findByContent(content: string): Promise { - return this.createQueryBuilder('post') - .where('post.content = :content', { content }) - .getOne(); - } - - async findByAuthor(authorId: string): Promise { - return this.createQueryBuilder('post') - .where('post.authorId = :author', { authorId }) - .getOne(); + const post = await this._repository.findOne({ where: { content } }); + return post ? PostMapper.toDomain(post) : null; } - async update( - criteria: FindOptionsWhere, - partialEntity: QueryDeepPartialEntity, + // Optionally, if you need to implement the update and delete methods: + async updatePost( + postId: string, + partialEntity: QueryDeepPartialEntity, ): Promise { - return this.createQueryBuilder('post') - .update(Post) - .set(partialEntity) - .where(criteria) - .execute(); - } - - delete(criteria: FindOptionsWhere): Promise { - return this.createQueryBuilder('post').delete().where(criteria).execute(); - } - - addEditor(postId: string, editorId: string) { - return this.createQueryBuilder('post') - .relation(Post, 'editors') - .of(postId) - .add(editorId); + return this._repository.update(postId, partialEntity); } - removeEditor(postId: string, editorId: string) { - return this.createQueryBuilder('post') - .relation(Post, 'editors') - .of(postId) - .remove(editorId); + async deletePost(postId: string): Promise { + return this._repository.delete(postId); } } From fd4079dc5ba6563d82fea767d3d1c0884f9ff150 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:22:07 +0300 Subject: [PATCH 10/26] Adding login middleware to ensure the user is logged in --- server/src/web/rest/middlewares/login.mw.ts | 114 +++++++++++--------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/server/src/web/rest/middlewares/login.mw.ts b/server/src/web/rest/middlewares/login.mw.ts index 440ea0f0..b0f3436f 100644 --- a/server/src/web/rest/middlewares/login.mw.ts +++ b/server/src/web/rest/middlewares/login.mw.ts @@ -1,50 +1,64 @@ -// import { JwtPayload, VerifyErrors } from 'jsonwebtoken'; -// import { ExpressHandler } from '../infrastucture/express-handler'; -// import { JwtService } from '../../../infrastructure/shared/jwt/jwt.service.impl'; -// import { UserRepository } from '@infrastructure/users'; - -// export const jwtParseMiddleware: ExpressHandler = async ( -// req, -// res, -// next, -// ) => { -// const jwtService = new JwtService(); -// const userRepo = new UserRepository(); -// const token = req.headers.authorization?.split(' ')[1]; -// if (!token) { -// return next(); -// } - -// let payload: JwtPayload; -// try { -// payload = jwtService.verify(token) as JwtPayload; -// } catch (e) { -// const verifyErr = e as VerifyErrors; -// if (verifyErr instanceof TokenExpiredError) { -// throw new TokenExpiredError(); -// } -// throw new BadTokenError(); -// } - -// const user = await userRepo -// .createQueryBuilder('user') -// .where('id = :id', {}) -// .getOne(); - -// if (!user) { -// throw new Error('User not found'); -// } -// res.locals.userId = user.id; -// return next(); -// }; - -// export const enforceJwtMiddleware: ExpressHandler = async ( -// _, -// res, -// next, -// ) => { -// if (!res.locals.userId) { -// return res.sendStatus(401); -// } -// return next(); -// }; +import { VerifyErrors } from 'jsonwebtoken'; +import { ExpressHandler } from '../infrastucture/express-handler'; +import { JwtService } from '../../../infrastructure/shared/jwt/jwt.service.impl'; +import { UserRepository } from '@infrastructure/users'; +import { container } from '@infrastructure/shared/ioc/inversify.config'; +import { + TokenExpiredError, + BadTokenError, +} from '@contracts/errors/unauthorized.error'; +import { DataSource } from 'typeorm'; +import { LOGGER } from '../logger'; +import { log } from 'console'; +import { JwtPayload } from '@contracts/services/IJwt'; + +class AuthMiddleware { + public static jwtParseMiddleware: ExpressHandler = async ( + req, + res, + next, + ) => { + const jwtService = new JwtService(); + const userRepository = new UserRepository(container.get(DataSource)); + const token = req.headers.authorization?.split(' ')[1]; + LOGGER.info(`Token: ${token}`); + if (!token) { + return next(); + } + + let payload: JwtPayload; + + try { + payload = jwtService.verify(token) as JwtPayload; + } catch (e) { + const verifyErr = e as VerifyErrors; + if (verifyErr instanceof TokenExpiredError) { + return next(new TokenExpiredError()); + } + return next(new BadTokenError()); + } + + const user = await userRepository.findById(payload.userId); + + if (!user) { + return next(new Error('User not found')); + } + + res.locals.userId = user.id; + res.locals.user = user; + return next(); + }; + + public static enforceJwtMiddleware: ExpressHandler = async ( + _, + res, + next, + ) => { + if (!res.locals.userId) { + return res.sendStatus(401); + } + return next(); + }; +} + +export { AuthMiddleware }; From ebe9ebfe8d4ae43a3aea74b51082830ae41a5856 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:23:03 +0300 Subject: [PATCH 11/26] Adding post controller (BUG) --- .../web/rest/controllers/posts.controller.ts | 48 +++++++++++++++++++ server/src/web/rest/routes.ts | 8 +++- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 server/src/web/rest/controllers/posts.controller.ts diff --git a/server/src/web/rest/controllers/posts.controller.ts b/server/src/web/rest/controllers/posts.controller.ts new file mode 100644 index 00000000..df2e68ba --- /dev/null +++ b/server/src/web/rest/controllers/posts.controller.ts @@ -0,0 +1,48 @@ +import { CreatePostRequest } from '@application/post'; +import { BaseUseCase } from '@application/shared'; +import { CreatePostResponseDto } from '@contracts/dtos/posts/create/create-post.response'; +import { TriggeredByUser } from '@domain/shared/entities'; +import { TYPES } from '@infrastructure/shared/ioc/types'; +import { inject, injectable } from 'inversify'; +import { ExpressHandler } from '../infrastucture/express-handler'; +import BaseController from './base.controller'; +import { LOGGER } from '../logger'; +import { POST_STATUS } from '@domain/eums/post-status.enum'; + +@injectable() +export class PostsController implements BaseController { + private _createPostUseCase: BaseUseCase< + CreatePostRequest, + CreatePostResponseDto + >; + constructor( + @inject(TYPES.ICreatePostInputPort) + _createPostInteractor: BaseUseCase< + CreatePostRequest, + CreatePostResponseDto + >, + ) { + this._createPostUseCase = _createPostInteractor; + } + + public create: ExpressHandler = + async (req, res) => { + LOGGER.info('PostsController.create'); + const { title, content } = req.body; + if (!title || !content) { + return res.status(400).json({}); + } + + const request = CreatePostRequest.create( + new TriggeredByUser(res.locals.userId, []), + title!, + content!, + res.locals.user.id, + res.locals.user, + [], + POST_STATUS.DRAFT, + ); + const result = await this._createPostUseCase.execute(request); + return res.json(result); + }; +} diff --git a/server/src/web/rest/routes.ts b/server/src/web/rest/routes.ts index 96b0454e..28708f73 100644 --- a/server/src/web/rest/routes.ts +++ b/server/src/web/rest/routes.ts @@ -1,15 +1,18 @@ -import 'reflect-metadata'; import { TYPES } from '@infrastructure/shared/ioc/types'; import { Application } from 'express'; import { inject, injectable } from 'inversify'; +import 'reflect-metadata'; import { AuthController } from './controllers/auth.controller'; import BaseController from './controllers/base.controller'; +import { PostsController } from './controllers/posts.controller'; import asyncWrapper from './infrastucture/async-wrapper'; +import { AuthMiddleware } from './middlewares/login.mw'; @injectable() export default class ApplicationRouter { constructor( @inject(TYPES.IAuthController) private authController: AuthController, + @inject(TYPES.IPostController) private postsController: PostsController, ) {} // We need to bind proper context to the controller methods private getController(context: BaseController, func: string) { @@ -22,5 +25,8 @@ export default class ApplicationRouter { this.getController(this.authController, 'register'), ); app.post('/auth/login', this.getController(this.authController, 'login')); + app + .use(AuthMiddleware.jwtParseMiddleware) + .post('/posts', this.getController(this.postsController, 'create')); } } From ae712734a800f546014f4bc828a9d36b2b27c696 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:23:50 +0300 Subject: [PATCH 12/26] Fixing Nullable lists problem --- server/src/infrastructure/users/user.mapper.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/infrastructure/users/user.mapper.ts b/server/src/infrastructure/users/user.mapper.ts index 1b767ea8..52f655bb 100644 --- a/server/src/infrastructure/users/user.mapper.ts +++ b/server/src/infrastructure/users/user.mapper.ts @@ -24,13 +24,13 @@ class UserMapper { createdAt: userPersistenceModel.createdAt, updatedAt: userPersistenceModel.updatedAt, deletedAt: userPersistenceModel.deletedAt, - comments: userPersistenceModel.comments.map(comment => + comments: userPersistenceModel.comments?.map(comment => CommentMapper.toDomain(comment), ), - likedPosts: userPersistenceModel.likedPosts.map(like => { + likedPosts: userPersistenceModel.likedPosts?.map(like => { return LikePostMapper.toDomain(like); }), - likedComments: userPersistenceModel.likedComments.map(like => + likedComments: userPersistenceModel.likedComments?.map(like => LikeCommentMapper.toDomain(like), ), equals: (user: User) => user.id === id, @@ -40,11 +40,11 @@ class UserMapper { deletedBy: userPersistenceModel.id, isPasswordMatched: (password: string) => userPersistenceModel.hashedPassword === password, - posts: userPersistenceModel.posts.map(post => PostMapper.toDomain(post)), - replies: userPersistenceModel.replies.map(reply => + posts: userPersistenceModel.posts?.map(post => PostMapper.toDomain(post)), + replies: userPersistenceModel.replies?.map(reply => ReplyMapper.toDomain(reply), ), - likedReplies: userPersistenceModel.likedReplies.map(like => + likedReplies: userPersistenceModel.likedReplies?.map(like => LikeReplyMapper.toDomain(like), ), }; From 37442a20424df2807298c63ec29de3ab516255f1 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:24:30 +0300 Subject: [PATCH 13/26] Adding JWT payload type to the implementation of jwt service --- server/src/infrastructure/shared/jwt/jwt.service.impl.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/src/infrastructure/shared/jwt/jwt.service.impl.ts b/server/src/infrastructure/shared/jwt/jwt.service.impl.ts index dea53fde..f7659a13 100644 --- a/server/src/infrastructure/shared/jwt/jwt.service.impl.ts +++ b/server/src/infrastructure/shared/jwt/jwt.service.impl.ts @@ -1,14 +1,15 @@ import jwt from 'jsonwebtoken'; import { injectable } from 'inversify'; import dotenv from 'dotenv'; -import { IJwtService } from '@contracts/services/IJwt'; +import { IJwtService, JwtPayload } from '@contracts/services/IJwt'; dotenv.config(); + @injectable() export class JwtService implements IJwtService { - sign = (payload: string): string => { + sign = (payload: JwtPayload): string => { return jwt.sign(payload, 'secret'); }; - verify = (token: string): string => { - return jwt.verify(token, 'secret') as string; + verify = (token: string): JwtPayload => { + return jwt.verify(token, 'secret') as JwtPayload; }; } From c13feec35a391bb330a41de6e75800704340d23a Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:25:09 +0300 Subject: [PATCH 14/26] Disable logs in typeorm configs --- server/src/infrastructure/shared/persistence/data-source.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/infrastructure/shared/persistence/data-source.ts b/server/src/infrastructure/shared/persistence/data-source.ts index ec123356..ad942408 100644 --- a/server/src/infrastructure/shared/persistence/data-source.ts +++ b/server/src/infrastructure/shared/persistence/data-source.ts @@ -51,7 +51,6 @@ const appDataSource = new DataSource({ type: 'sqlite', database: 'db.sqlite3', synchronize: true, - logging: true, entities: [ UserPersistence, LikePostPersistence, From 3ed314b8f89dff3a6998d75a610f8c84eedffd22 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:25:43 +0300 Subject: [PATCH 15/26] Adding post bindings --- server/src/infrastructure/shared/ioc/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/infrastructure/shared/ioc/types.ts b/server/src/infrastructure/shared/ioc/types.ts index 6f53ca6b..c89cfb01 100644 --- a/server/src/infrastructure/shared/ioc/types.ts +++ b/server/src/infrastructure/shared/ioc/types.ts @@ -4,12 +4,17 @@ let TYPES = { IHasherService: Symbol.for('IHasherService'), ILoginInputPort: Symbol.for('ILoginInputPort'), IRegisterInputPort: Symbol.for('IRegisterInputPort'), + + ICreatePostInputPort: Symbol.for('ICreatePostInputPort'), + ICreatePostOutputPort: Symbol.for('ICreatePostOutputPort'), + ILoginOutputPort: Symbol.for('ILoginOutputPort'), IRegisterOutputPort: Symbol.for('IRegisterOutputPort'), IUserRepository: Symbol.for('IUserRepository'), IPostRepository: Symbol.for('IPostRepository'), IUserController: Symbol.for('IUserController'), IAuthController: Symbol.for('IAuthController'), + IPostController: Symbol.for('IPostController'), ApplicationRouter: Symbol.for('ApplicationRouter'), DataSource: Symbol.for('DataSource'), IUserMapper: Symbol.for('IMapper'), From e9cb4b87aeebbbc914ad2f8972a9bfdcccdc3a90 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Mon, 27 May 2024 01:26:00 +0300 Subject: [PATCH 16/26] cleanup --- .../[locale]/(unauth)/api/guestbook/route.ts | 178 +++++++++--------- client/src/models/Schema.ts | 18 +- server/db.sqlite3 | Bin 126976 -> 126976 bytes .../post/create-old/create-post.usecase.ts | 60 +++--- server/src/application/post/index.ts | 8 +- server/src/domain/entities/posts/post.ts | 28 +++ server/src/web/process.log | 11 +- server/src/web/rest/app.ts | 5 - 8 files changed, 163 insertions(+), 145 deletions(-) diff --git a/client/src/app/[locale]/(unauth)/api/guestbook/route.ts b/client/src/app/[locale]/(unauth)/api/guestbook/route.ts index 6d069ab0..380a8662 100644 --- a/client/src/app/[locale]/(unauth)/api/guestbook/route.ts +++ b/client/src/app/[locale]/(unauth)/api/guestbook/route.ts @@ -1,89 +1,89 @@ -import { eq, sql } from 'drizzle-orm'; -import { NextResponse } from 'next/server'; - -import { db } from '@/libs/DB'; -import { logger } from '@/libs/Logger'; -import { guestbookSchema } from '@/models/Schema'; -import { - DeleteGuestbookValidation, - EditGuestbookValidation, - GuestbookValidation, -} from '@/validations/GuestbookValidation'; - -export const POST = async (request: Request) => { - const json = await request.json(); - const parse = GuestbookValidation.safeParse(json); - - if (!parse.success) { - return NextResponse.json(parse.error.format(), { status: 422 }); - } - - try { - const guestbook = await db - .insert(guestbookSchema) - .values(parse.data) - .returning(); - - logger.info('A new guestbook has been created'); - - return NextResponse.json({ - id: guestbook[0]?.id, - }); - } catch (error) { - logger.error(error, 'An error occurred while creating a guestbook'); - - return NextResponse.json({}, { status: 500 }); - } -}; - -export const PUT = async (request: Request) => { - const json = await request.json(); - const parse = EditGuestbookValidation.safeParse(json); - - if (!parse.success) { - return NextResponse.json(parse.error.format(), { status: 422 }); - } - - try { - await db - .update(guestbookSchema) - .set({ - ...parse.data, - updatedAt: sql`(strftime('%s', 'now'))`, - }) - .where(eq(guestbookSchema.id, parse.data.id)) - .run(); - - logger.info('A guestbook entry has been updated'); - - return NextResponse.json({}); - } catch (error) { - logger.error(error, 'An error occurred while updating a guestbook'); - - return NextResponse.json({}, { status: 500 }); - } -}; - -export const DELETE = async (request: Request) => { - const json = await request.json(); - const parse = DeleteGuestbookValidation.safeParse(json); - - if (!parse.success) { - return NextResponse.json(parse.error.format(), { status: 422 }); - } - - try { - await db - .delete(guestbookSchema) - .where(eq(guestbookSchema.id, parse.data.id)) - .run(); - - logger.info('A guestbook entry has been deleted'); - - return NextResponse.json({}); - } catch (error) { - logger.error(error, 'An error occurred while deleting a guestbook'); - - return NextResponse.json({}, { status: 500 }); - } -}; +// import { eq, sql } from 'drizzle-orm'; +// import { NextResponse } from 'next/server'; + +// import { db } from '@/libs/DB'; +// import { logger } from '@/libs/Logger'; +// import { guestbookSchema } from '@/models/Schema'; +// import { +// DeleteGuestbookValidation, +// EditGuestbookValidation, +// GuestbookValidation, +// } from '@/validations/GuestbookValidation'; + +// export const POST = async (request: Request) => { +// const json = await request.json(); +// const parse = GuestbookValidation.safeParse(json); + +// if (!parse.success) { +// return NextResponse.json(parse.error.format(), { status: 422 }); +// } + +// try { +// const guestbook = await db +// .insert(guestbookSchema) +// .values(parse.data) +// .returning(); + +// logger.info('A new guestbook has been created'); + +// return NextResponse.json({ +// id: guestbook[0]?.id, +// }); +// } catch (error) { +// logger.error(error, 'An error occurred while creating a guestbook'); + +// return NextResponse.json({}, { status: 500 }); +// } +// }; + +// export const PUT = async (request: Request) => { +// const json = await request.json(); +// const parse = EditGuestbookValidation.safeParse(json); + +// if (!parse.success) { +// return NextResponse.json(parse.error.format(), { status: 422 }); +// } + +// try { +// await db +// .update(guestbookSchema) +// .set({ +// ...parse.data, +// updatedAt: sql`(strftime('%s', 'now'))`, +// }) +// .where(eq(guestbookSchema.id, parse.data.id)) +// .run(); + +// logger.info('A guestbook entry has been updated'); + +// return NextResponse.json({}); +// } catch (error) { +// logger.error(error, 'An error occurred while updating a guestbook'); + +// return NextResponse.json({}, { status: 500 }); +// } +// }; + +// export const DELETE = async (request: Request) => { +// const json = await request.json(); +// const parse = DeleteGuestbookValidation.safeParse(json); + +// if (!parse.success) { +// return NextResponse.json(parse.error.format(), { status: 422 }); +// } + +// try { +// await db +// .delete(guestbookSchema) +// .where(eq(guestbookSchema.id, parse.data.id)) +// .run(); + +// logger.info('A guestbook entry has been deleted'); + +// return NextResponse.json({}); +// } catch (error) { +// logger.error(error, 'An error occurred while deleting a guestbook'); + +// return NextResponse.json({}, { status: 500 }); +// } +// }; diff --git a/client/src/models/Schema.ts b/client/src/models/Schema.ts index 0941fe1c..5f5cf587 100644 --- a/client/src/models/Schema.ts +++ b/client/src/models/Schema.ts @@ -2,13 +2,13 @@ import { sql } from 'drizzle-orm'; import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; export const guestbookSchema = sqliteTable('guestbook', { - id: integer('id').primaryKey(), - username: text('username').notNull(), - body: text('body').notNull(), - createdAt: integer('created_at', { mode: 'timestamp' }).default( - sql`(strftime('%s', 'now'))`, - ), - updatedAt: integer('updated_at', { mode: 'timestamp' }).default( - sql`(strftime('%s', 'now'))`, - ), + id: integer('id').primaryKey(), + username: text('username').notNull(), + body: text('body').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).default( + sql`(strftime('%s', 'now'))`, + ), + updatedAt: integer('updated_at', { mode: 'timestamp' }).default( + sql`(strftime('%s', 'now'))`, + ), }); diff --git a/server/db.sqlite3 b/server/db.sqlite3 index 256312a9c816e6ecc07e1aa6f23831b512feeaed..324447922b100544d5f02a617956a1d8ac292634 100644 GIT binary patch delta 619 zcmZp8z~1nHeS);0ECU0BJQTA4Y3+$R#*DHX6PCPZW8`;c;CJ3EC}7E7-)P0i6Dq4M zE8fV;X>H7qYLRMTXl!Vtn`mO3s%v6qlBkBd@G50M4 z|68EkV}4;FW^GP1?`{iVWDSsH%3$DM&uh-}f&Cx*UREcT|4bP?%X!jxRJY45VDw_* zY8BCC7uVHg?2Mj%a1~?r^#6+(&8Ht(#ArR;Y%!zHWMS@(=}n6nefdlCG7C#n<4cQE zi}Dh4Q>U+4%owbzqu}iC7aS7g=;;@tpafNtnwyxJqofe(=NTC4s-U3+;cIGc|G$Ls zHwz2T3nuN!4;gKyTdiWO1;#s*<@VjH7_%6e>HdtMtNgNNgz`Kh+#BHUV#5G|4m?k$nZ~hdC#~90N)#_tN;K2 delta 4856 zcmd6r%WoUU9mlmYW$M9)RHu+-Rk0l_u8nqAGdsJpyL8%^6eW?gNKvFnJ>tgQCm$k3 ziXugdsIg|zTQ5xom_U0;Qln_(0BzBtI`s8Fv;o>f0;HE-a!76k>Z0i^MM1Qr#H!f< zJ&3#daY%h;_?_Q;s0Sa~AAD&4!T89fo}Qk|?H?Pt-hAWhh`RL4Z$J9@`iSjekNd~2 zk6gE$yN(YW*N478`1iqA2Ojo+(EpM>b$+k!qrRl=;jc!9b2hW_jS)xU7V4Q8y*$OQ1x?>pbVP0x@*6^h+t$@AFlr$`$tsf&Ove3lshku^gu^Q{{?!_@T+Hrx7wRkd zwAU9{4$cLug;ILj&oBDxfqG_Ek3?2gx$&i8#|1Khk@3;5wI+Z%K%nG-q6c9HNa88w zaK-e=8)><^LpZgAhZa^R)q>~`mACHHl_@PJlyY~3nu;Vw$}U&+Xt1^paj`0;*JCo+ zC@In92&eMZjNEv|-CEX>@oSJ!X;?wJ#9N})wzf4L+H81S?Fn49av>WK0U*h=s_6m z6L@&cASj$tKEgkwKY%~M?whM^u|mGea;)DEHxw`MmE=keFT<@^9)(w8yctYIH$r?` zOYj=Zd293Wg-koh^*T6$<_6ch@>OkZr5)N8NBFR0HbeaH*v?^*CVg<9NBFM+!MGu6Gm71sLWR;J> zAGDH#M$)-sm+B#9E@3G3)!A6Fx|jjE6qwbO?HUvGsf7^8d9_lYhLxC4G=)^CDkQ_x z$#~=Xpd*1$kJCB+a-)wD1)xY+S81S=*mDLi&@vVfjd`euKt()9W_E-KI1d0GuCvP= zmSioT*~E0G;9V&&!D#^gYobnF@-m1IFi0t7S!h>(&lO-q8I&^lz1Btn2ir85Ztkz{-L zgS?vuef+R?fJ#V@fd-b6ijYYMeW`fCUk!!VS7sLj`cfd3T#2d>PG!XvA(>kWmrL30 z8dq8f8Ag7gxA9Vcn{)Ik;gl61VT?%_fGpCSE@-r3s)j_fkW^SrQ#HOb3>FmcVV_#% zcB#dg%N?VPuAuySG`k&(Z6p)Q;{2i_>6?{mU5VA>vX=|__1Wxd%r`d^D#&r6S}{a@ zG!pM^T(mpJ$Ok_$K7NyA7Lt~WnvK_}7nb_H@ zhMZ*Z1UbmY$$Q?TMy-dL>m$y`J??+GC!Oy(AG`nTV%&SKcidC1Ke?W``drKIhVy;b z1NRC!&Kpki#<>J_mNR_Be(nMph=#2feCFxYaX7iHfzLbzOmYmktdXDHagQ3V(;9e| z?&h!tKC_jOPBPS*|5x&Mh)Gx8HE+`RUPJiPsg0`{A6aDFu9ZF={pi_Ir*QI~q}q1M$tHt^4$ z8|`NtWj$Xkl;qORy;rAp+A5*{k4|hu$b^rhX diff --git a/server/src/application/post/create-old/create-post.usecase.ts b/server/src/application/post/create-old/create-post.usecase.ts index dd92194a..069a5c8f 100644 --- a/server/src/application/post/create-old/create-post.usecase.ts +++ b/server/src/application/post/create-old/create-post.usecase.ts @@ -1,32 +1,32 @@ -import { BaseUseCase } from '@application/shared'; -import { CreatePostRequestDto } from '@contracts/dtos/posts/create/create-post.request'; -import { CreatePostResponseDto } from '@contracts/dtos/posts/create/create-post.response'; -import { Post } from '@domain/entities'; -import { IPostRepository } from '@domain/repositories/post.repository'; +// import { BaseUseCase } from '@application/shared'; +// import { CreatePostResponseDto } from '@contracts/dtos/posts/create/create-post.response'; +// import { Post } from '@domain/entities'; +// import { IPostRepository } from '@domain/repositories/post.repository'; +// import { CreatePostRequest } from '../create/create-post.request'; -class CreatePostUseCase extends BaseUseCase< - CreatePostRequestDto, - CreatePostResponseDto -> { - protected async performOperation( - request: CreatePostRequestDto, - ): Promise { - const post = Post.create( - request.title, - request.content, - request.authorId, - request.author, - ); - const createdPost = await this._postRepository.createPost(post); - const result: CreatePostResponseDto = { - author: createdPost.author, - post: createdPost, - }; - return result; - } - constructor(private _postRepository: IPostRepository) { - super(); - } -} +// class CreatePostUseCase extends BaseUseCase< +// CreatePostRequest, +// CreatePostResponseDto +// > { +// protected async performOperation( +// request: CreatePostRequest, +// ): Promise { +// const post = Post.create( +// request.title, +// request.content, +// request.authorId, +// request.author, +// ); +// const createdPost = await this._postRepository.createPost(post); +// const result: CreatePostResponseDto = { +// author: createdPost.author, +// post: createdPost, +// }; +// return result; +// } +// constructor(private _postRepository: IPostRepository) { +// super(); +// } +// } -export { CreatePostUseCase }; +// export { CreatePostUseCase }; diff --git a/server/src/application/post/index.ts b/server/src/application/post/index.ts index 5087f2d5..7db87d09 100644 --- a/server/src/application/post/index.ts +++ b/server/src/application/post/index.ts @@ -1,5 +1,5 @@ export * from './create'; -export * from './edit'; -export * from './delete'; -export * from './find-all'; -export * from './find-by-id'; +// export * from './edit'; +// export * from './delete'; +// export * from './find-all'; +// export * from './find-by-id'; diff --git a/server/src/domain/entities/posts/post.ts b/server/src/domain/entities/posts/post.ts index ad302749..9ef4aff7 100644 --- a/server/src/domain/entities/posts/post.ts +++ b/server/src/domain/entities/posts/post.ts @@ -21,6 +21,34 @@ class Post extends AuditableBaseEntity { ) { super(id, createdAt, authorId, updatedAt, authorId, deletedAt, authorId); } + + public static create( + id: Nullable, + title: string, + content: string, + authorId: string, + author: User, + likes: Nullable, + comments: Nullable, + status: POST_STATUS, + createdAt: Date, + updatedAt: Nullable, + deletedAt: Nullable, + ) { + return new Post( + id, + title, + content, + authorId, + author, + likes, + comments, + status, + createdAt, + updatedAt, + deletedAt, + ); + } } export { Post }; diff --git a/server/src/web/process.log b/server/src/web/process.log index 2bd0db66..e4c8bce2 100644 --- a/server/src/web/process.log +++ b/server/src/web/process.log @@ -1,8 +1,3 @@ -{"level":30,"time":1716586314081,"msg":"Application is running on port 2105 🚀"} -{"level":30,"time":1716586314084,"msg":"Environment: development"} -{"level":30,"time":1716586314131,"msg":"Loaded entities"} -{"level":30,"time":1716586314132,"msg":7} -{"level":30,"time":1716586314132,"msg":1} -{"level":30,"time":1716586314132,"msg":"Data source initialized"} -{"level":50,"time":1716586315655,"err":{"type":"QueryFailedError","message":"SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.email","stack":"QueryFailedError: SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.email\n at Statement.handler (/home/modsyan/projects/bug-zone/server/node_modules/.pnpm/typeorm@0.3.20_ioredis@5.4.1_sqlite3@5.1.7_ts-node@10.9.2/node_modules/typeorm/src/driver/sqlite/SqliteQueryRunner.ts:135:29)","query":"INSERT INTO \"user_persistence\"(\"id\", \"username\", \"email\", \"firstName\", \"lastName\", \"hashedPassword\", \"role\", \"createdAt\", \"updatedAt\", \"deletedAt\") VALUES (?, ?, ?, ?, ?, ?, 1, ?, datetime('now'), NULL)","parameters":["ef7269d7-168f-425c-963f-6bef57fa5ce0","devbenho","benho@benho.com","Muhammad","Benho","$2b$10$OSOkbp869oiHOOGI8JKTBugLoAqdNGSj8drV1szDam0EHEbKQvQUa","2024-05-24 21:31:55.596"],"driverError":{"type":"Error","message":"SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.email","stack":"Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.email","errno":19,"code":"SQLITE_CONSTRAINT"},"errno":19,"code":"SQLITE_CONSTRAINT"},"msg":"SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.email"} -{"level":50,"time":1716586356139,"err":{"type":"QueryFailedError","message":"SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.username","stack":"QueryFailedError: SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.username\n at Statement.handler (/home/modsyan/projects/bug-zone/server/node_modules/.pnpm/typeorm@0.3.20_ioredis@5.4.1_sqlite3@5.1.7_ts-node@10.9.2/node_modules/typeorm/src/driver/sqlite/SqliteQueryRunner.ts:135:29)","query":"INSERT INTO \"user_persistence\"(\"id\", \"username\", \"email\", \"firstName\", \"lastName\", \"hashedPassword\", \"role\", \"createdAt\", \"updatedAt\", \"deletedAt\") VALUES (?, ?, ?, ?, ?, ?, 1, ?, datetime('now'), NULL)","parameters":["44a5fbe9-8e53-4a1e-8bb1-0d0211703257","devbenho","benh3o@benho.com","Muhammad","Benho","$2b$10$Vm6lfh9eLvDnTk8Qy6Jsu.aVi12iPSEt1M0S14NK3tviqdfHMDDpi","2024-05-24 21:32:36.085"],"driverError":{"type":"Error","message":"SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.username","stack":"Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.username","errno":19,"code":"SQLITE_CONSTRAINT"},"errno":19,"code":"SQLITE_CONSTRAINT"},"msg":"SQLITE_CONSTRAINT: UNIQUE constraint failed: user_persistence.username"} +{"level":30,"time":1716762101785,"msg":"Application is running on port 2105 🚀"} +{"level":30,"time":1716762101792,"msg":"Environment: development"} +{"level":30,"time":1716762101813,"msg":"Data source initialized"} diff --git a/server/src/web/rest/app.ts b/server/src/web/rest/app.ts index 4a0396cd..305c8712 100644 --- a/server/src/web/rest/app.ts +++ b/server/src/web/rest/app.ts @@ -14,11 +14,6 @@ app.use(express.urlencoded({ extended: true })); appDataSource .initialize() .then(() => { - // LOGGER.info(appDataSource.subscribers); - // log the loaded entities - LOGGER.info('Loaded entities'); - LOGGER.info(appDataSource.entityMetadatas.length); - LOGGER.info(appDataSource.subscribers.length); LOGGER.info('Data source initialized'); }) .catch(err => { From fdd4cec78821704fdcc8a4d3e5d1933791b67787 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Thu, 30 May 2024 15:22:03 +0300 Subject: [PATCH 17/26] Adding find all posts use-case feature at application layer --- .../post/find-all/find-all-post.request.ts | 36 +++++++++++++++++++ .../post/find-all/find-all-post.usecase.ts | 34 ++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 server/src/application/post/find-all/find-all-post.request.ts create mode 100644 server/src/application/post/find-all/find-all-post.usecase.ts diff --git a/server/src/application/post/find-all/find-all-post.request.ts b/server/src/application/post/find-all/find-all-post.request.ts new file mode 100644 index 00000000..79884c80 --- /dev/null +++ b/server/src/application/post/find-all/find-all-post.request.ts @@ -0,0 +1,36 @@ +import { UseCaseRequest } from '@application/shared'; +import { TriggeredBy } from '@domain/shared/entities/triggered-by'; +import { InvalidParameterException } from '@domain/shared/exceptions'; + +class FindAllPostRequest extends UseCaseRequest { + readonly pageSize: number; + readonly pageNumber: number; + + // Constructor Section + constructor(triggeredBy: TriggeredBy, pageSize: number, pageNumber: number) { + super(triggeredBy); + this.pageSize = pageSize; + this.pageNumber = pageNumber; + } + + public static create( + triggeredBy: TriggeredBy, + pageSize: number, + pageNumber: number, + ): FindAllPostRequest { + return new FindAllPostRequest(triggeredBy, pageSize, pageNumber); + } + + // Validate here using EnsureClass + protected validatePayload(): void { + if (this.pageSize <= 0) { + throw new InvalidParameterException('Page size must be greater than 0'); + } + + if (this.pageNumber <= 0) { + throw new InvalidParameterException('Page number must be greater than 0'); + } + } +} + +export { FindAllPostRequest }; diff --git a/server/src/application/post/find-all/find-all-post.usecase.ts b/server/src/application/post/find-all/find-all-post.usecase.ts new file mode 100644 index 00000000..af3a8c28 --- /dev/null +++ b/server/src/application/post/find-all/find-all-post.usecase.ts @@ -0,0 +1,34 @@ +import { BaseUseCase } from '@application/shared'; +import { FindAllPostRequest } from './find-all-post.request'; +import { PostResponseDto } from '@contracts/dtos/posts'; +import { TYPES } from '@infrastructure/shared/ioc/types'; +import { IPostRepository } from '@domain/repositories/post.repository'; +import { inject } from 'inversify'; + +class FindAllPostUseCase extends BaseUseCase< + FindAllPostRequest, + PostResponseDto[] +> { + constructor( + @inject(TYPES.IPostRepository) + private readonly _postRepository: IPostRepository, + ) { + super(); + } + + public async performOperation( + request: FindAllPostRequest, + ): Promise { + const { pageSize, pageNumber } = request; + const posts = await this._postRepository.findAll(pageSize, pageNumber); + const postDtos = await Promise.all( + posts.map(async post => { + return await PostResponseDto.fromEntity(post); + }), + ); + + return postDtos; + } +} + +export { FindAllPostUseCase }; From 9169ee9afbbb78cb684a192739a2e1ad9f02c9b9 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Thu, 30 May 2024 15:23:18 +0300 Subject: [PATCH 18/26] Adding find all posts use-case dependencies to IOC container --- server/src/infrastructure/shared/ioc/inversify.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/infrastructure/shared/ioc/inversify.config.ts b/server/src/infrastructure/shared/ioc/inversify.config.ts index 8fadea2b..06d7aad5 100644 --- a/server/src/infrastructure/shared/ioc/inversify.config.ts +++ b/server/src/infrastructure/shared/ioc/inversify.config.ts @@ -14,6 +14,7 @@ import { appDataSource } from '@infrastructure/shared/persistence/data-source'; import { PostsController } from '@/web/rest/controllers/posts.controller'; import { PostRepository } from '@infrastructure/posts'; import { CreatePostUseCase } from '@application/post'; +import { FindAllPostUseCase } from '@application/post/find-all/find-all-post.usecase'; const container = new Container(); // Bind the extrernal dependencies @@ -30,6 +31,7 @@ container.bind(TYPES.ILoginInputPort).to(LoginUseCase); container.bind(TYPES.IRegisterInputPort).to(RegisterUsecase); container.bind(TYPES.ICreatePostInputPort).to(CreatePostUseCase); +container.bind(TYPES.IFindAllPostInputPort).to(FindAllPostUseCase); // Bind the controllers container.bind(TYPES.IAuthController).to(AuthController); From c48e7b360331039679758e09e3dfaf9bc46543b9 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Thu, 30 May 2024 15:23:36 +0300 Subject: [PATCH 19/26] Login use-case cleanup --- server/src/application/auth/login/login.use-case.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/application/auth/login/login.use-case.ts b/server/src/application/auth/login/login.use-case.ts index 001a4694..01f07b15 100644 --- a/server/src/application/auth/login/login.use-case.ts +++ b/server/src/application/auth/login/login.use-case.ts @@ -19,7 +19,6 @@ class LoginUseCase extends BaseUseCase { public async performOperation( request: AuthRequestDto, ): Promise { - log('request login use case', request); const user = (await this._userRepository.findByEmail(request.login)) || (await this._userRepository.findByUsername(request.login)); From 0cbae9323b1855732cd7c38d97598959f1f17aa5 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Thu, 30 May 2024 15:23:55 +0300 Subject: [PATCH 20/26] Create post use-case cleanup --- server/src/application/post/create/create-post.usecase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/application/post/create/create-post.usecase.ts b/server/src/application/post/create/create-post.usecase.ts index 6cce5c2b..f8e265a1 100644 --- a/server/src/application/post/create/create-post.usecase.ts +++ b/server/src/application/post/create/create-post.usecase.ts @@ -1,4 +1,4 @@ -import { BaseUseCase, UseCase } from '@application/shared'; +import { BaseUseCase } from '@application/shared'; import { CreatePostRequest } from './create-post.request'; import { PostDetailsResponseDto } from '@contracts/dtos/posts'; import { inject, injectable } from 'inversify'; From bfb5fb76afe4ae49bddfdf60a99d6ca9979bd50d Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Thu, 30 May 2024 15:24:46 +0300 Subject: [PATCH 21/26] Removing nullable wrapper from lists | Domain entites clean --- server/src/domain/entities/comment.ts | 4 ++-- server/src/domain/entities/posts/post.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/domain/entities/comment.ts b/server/src/domain/entities/comment.ts index 384f773a..cca4e5e9 100644 --- a/server/src/domain/entities/comment.ts +++ b/server/src/domain/entities/comment.ts @@ -13,8 +13,8 @@ export class Comment extends AuditableBaseEntity { public authorId: string, public author: User, public content: string, - public replies: Nullable, - public likes: Nullable, + public replies: Reply[], + public likes: LikeComment[], public createdAt: Date, public updatedAt: Nullable, public deletedAt: Nullable, diff --git a/server/src/domain/entities/posts/post.ts b/server/src/domain/entities/posts/post.ts index 9ef4aff7..2cb56981 100644 --- a/server/src/domain/entities/posts/post.ts +++ b/server/src/domain/entities/posts/post.ts @@ -12,8 +12,8 @@ class Post extends AuditableBaseEntity { public content: string, public authorId: string, public author: User, - public likes: Nullable, - public comments: Nullable, + public likes: LikePost[], + public comments: Comment[], public status: POST_STATUS = POST_STATUS.DRAFT, public createdAt: Date, public updatedAt: Nullable, @@ -28,8 +28,8 @@ class Post extends AuditableBaseEntity { content: string, authorId: string, author: User, - likes: Nullable, - comments: Nullable, + likes: LikePost[], + comments: Comment[], status: POST_STATUS, createdAt: Date, updatedAt: Nullable, From 160784ede4b23317473e5bd4a6b9e255bb235d0e Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Thu, 30 May 2024 15:25:51 +0300 Subject: [PATCH 22/26] Adding promises to enable lazy loading --- .../comments/comment.persistence.ts | 2 +- .../like-replies/like-reply.persistence.ts | 5 +-- .../infrastructure/posts/post.persistence.ts | 6 +-- .../infrastructure/users/user.persistence.ts | 43 +++++++++---------- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/server/src/infrastructure/comments/comment.persistence.ts b/server/src/infrastructure/comments/comment.persistence.ts index 26f96f60..f3b4e51f 100644 --- a/server/src/infrastructure/comments/comment.persistence.ts +++ b/server/src/infrastructure/comments/comment.persistence.ts @@ -29,7 +29,7 @@ class CommentPersistence { deletedAt: Nullable; @Column() - userId: Nullable; + userId: string; @ManyToOne(() => UserPersistence, user => user.comments, { lazy: true }) user: UserPersistence; diff --git a/server/src/infrastructure/like-replies/like-reply.persistence.ts b/server/src/infrastructure/like-replies/like-reply.persistence.ts index 9558c714..52129a51 100644 --- a/server/src/infrastructure/like-replies/like-reply.persistence.ts +++ b/server/src/infrastructure/like-replies/like-reply.persistence.ts @@ -9,7 +9,6 @@ import { UpdateDateColumn, } from 'typeorm'; import { ReplyPersistence } from '@infrastructure/replies/reply.persistence'; -import { BaseEntity } from '@infrastructure/shared/persistence/entities/base.persistence'; import { Nullable } from '@domain/shared/types'; @Entity() @@ -36,9 +35,7 @@ class LikeReplyPersistence { replyId: string; @ManyToOne(() => ReplyPersistence, reply => reply.likes, { lazy: true }) - reply: ReplyPersistence; - - + reply: Promise; } export { LikeReplyPersistence }; diff --git a/server/src/infrastructure/posts/post.persistence.ts b/server/src/infrastructure/posts/post.persistence.ts index 132f4542..b1557887 100644 --- a/server/src/infrastructure/posts/post.persistence.ts +++ b/server/src/infrastructure/posts/post.persistence.ts @@ -36,16 +36,16 @@ class PostPersistence { @Column() authorId: string; - @ManyToOne(() => UserPersistence, user => user.posts, { lazy: true }) + @ManyToOne(() => UserPersistence, user => user.posts, { eager: true }) author: UserPersistence; @OneToMany(() => CommentPersistence, comment => comment.post, { lazy: true }) - comments: CommentPersistence[]; + comments: Promise; @OneToMany(() => LikePostPersistence, likePost => likePost.post, { lazy: true, }) - likes: LikePostPersistence[]; + likes: Promise; // add status column enum ['draft', 'published', 'deleted'] @Column() diff --git a/server/src/infrastructure/users/user.persistence.ts b/server/src/infrastructure/users/user.persistence.ts index 8f62ad56..785562d3 100644 --- a/server/src/infrastructure/users/user.persistence.ts +++ b/server/src/infrastructure/users/user.persistence.ts @@ -9,6 +9,8 @@ import { CreateDateColumn, UpdateDateColumn, DeleteDateColumn, + JoinColumn, + JoinTable, } from 'typeorm'; import { CommentPersistence } from '../comments/comment.persistence'; import { PostPersistence } from '../posts/post.persistence'; @@ -53,33 +55,30 @@ class UserPersistence { @DeleteDateColumn({ nullable: true }) public deletedAt: Nullable; - @OneToMany( - () => LikePostPersistence, - likePost => likePost.user /** {lazy: true,}*/, - ) - public likedPosts: LikePostPersistence[]; + @OneToMany(() => LikePostPersistence, likePost => likePost.user) + @JoinTable() + public likedPosts: Promise; - @OneToMany(() => ReplyPersistence, reply => reply.userId /** {lazy: true,}*/) - public replies: ReplyPersistence[]; + @OneToMany(() => ReplyPersistence, reply => reply.user /** {lazy: true,}*/) + @JoinTable() + public replies: Promise; - @OneToMany( - () => CommentPersistence, - comment => comment.user /** {lazy: true,}*/, - ) - public comments: CommentPersistence[]; + @OneToMany(() => CommentPersistence, comment => comment.user) + @JoinTable() + public comments: Promise; - @OneToMany(() => PostPersistence, post => post.author /** {lazy: true,}*/) - public posts: PostPersistence[]; + @OneToMany(() => PostPersistence, post => post.author) + @JoinTable() + public posts: Promise; // add likeComment - @OneToMany( - () => LikeCommentPersistence, - likeComment => likeComment.user /** {lazy: true,}*/, - ) - public likedComments: LikeCommentPersistence[]; - - @OneToMany(() => LikeReplyPersistence, likePost => likePost.user, {}) - public likedReplies: LikeReplyPersistence[]; + @OneToMany(() => LikeCommentPersistence, likeComment => likeComment.user) + @JoinTable() + public likedComments: Promise; + + @OneToMany(() => LikeReplyPersistence, likePost => likePost.user) + @JoinTable() + public likedReplies: Promise; } export { UserPersistence }; From 2d1b457151f78db687f390b30aa0afc214f26d80 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Thu, 30 May 2024 15:26:46 +0300 Subject: [PATCH 23/26] Handling lazy loading entities (promises) mapping --- .../infrastructure/comments/comment.mapper.ts | 47 ++++- .../like-comments/like-comment.mapper.ts | 11 +- .../like-posts/like-post.mapper.ts | 23 +-- .../like-replies/like-reply.mapper.ts | 30 +++- .../src/infrastructure/posts/post.mapper.ts | 67 +++++-- .../posts/post.repository.impl.ts | 24 ++- .../infrastructure/replies/reply.mapper.ts | 33 +++- .../src/infrastructure/users/user.mapper.ts | 170 +++++++++--------- .../users/user.repository.impl.ts | 5 +- 9 files changed, 247 insertions(+), 163 deletions(-) diff --git a/server/src/infrastructure/comments/comment.mapper.ts b/server/src/infrastructure/comments/comment.mapper.ts index de1a9cbb..5e38d719 100644 --- a/server/src/infrastructure/comments/comment.mapper.ts +++ b/server/src/infrastructure/comments/comment.mapper.ts @@ -4,26 +4,55 @@ import { UserMapper } from '@infrastructure/users'; import { PostMapper } from '@infrastructure/posts'; import { ReplyMapper } from '@infrastructure/replies'; import { LikeCommentMapper } from '@infrastructure/like-comments'; -import { LikeCommentPersistence, ReplyPersistence } from '..'; export class CommentMapper { - public static toDomain(commentPer: CommentPersistence): Comment { + public static async toDomain( + commentPer: CommentPersistence, + ): Promise { + const replies = await Promise.all( + commentPer.replies.map(reply => ReplyMapper.toDomain(reply)), + ); + const likes = await Promise.all( + commentPer.likes.map(like => LikeCommentMapper.toDomain(like)), + ); + return new Comment( commentPer.id, commentPer.postId, - PostMapper.toDomain(commentPer.post), - commentPer.userId as string, - UserMapper.toDomain(commentPer.user), + await PostMapper.toDomain(commentPer.post), + commentPer.userId, + await UserMapper.toDomain(commentPer.user), commentPer.content, - commentPer.replies.map(reply => ReplyMapper.toDomain(reply)), - commentPer.likes.map(like => LikeCommentMapper.toDomain(like)), + replies, + likes, commentPer.createdAt, commentPer.updatedAt, commentPer.deletedAt, ); } - public static toPersistence(comment: Comment): CommentPersistence { - return new CommentPersistence(); + public static async toPersistence( + comment: Comment, + ): Promise { + const commentPersistence = new CommentPersistence(); + + if (comment.id != null) { + commentPersistence.id = comment.id; + } + commentPersistence.postId = comment.postId; + commentPersistence.userId = comment.authorId; + commentPersistence.content = comment.content; + commentPersistence.createdAt = comment.createdAt; + commentPersistence.deletedAt = comment.deletedAt; + commentPersistence.replies = await Promise.all( + comment.replies.map(reply => ReplyMapper.toPersistence(reply)), + ); + commentPersistence.likes = await Promise.all( + comment.likes.map(like => LikeCommentMapper.toPersistence(like)), + ); + commentPersistence.post = await PostMapper.toPersistence(comment.post); + commentPersistence.user = await UserMapper.toPersistence(comment.author); + + return commentPersistence; } } diff --git a/server/src/infrastructure/like-comments/like-comment.mapper.ts b/server/src/infrastructure/like-comments/like-comment.mapper.ts index c8545abc..87b85126 100644 --- a/server/src/infrastructure/like-comments/like-comment.mapper.ts +++ b/server/src/infrastructure/like-comments/like-comment.mapper.ts @@ -4,13 +4,18 @@ import { LikeComment } from '@domain/entities/like-comment'; import { CommentMapper } from '@infrastructure/comments/comment.mapper'; class LikeCommentMapper { - static toDomain(likeCommentPer: LikeCommentPersistence): LikeComment { + static async toDomain( + likeCommentPer: LikeCommentPersistence, + ): Promise { + const comment = await CommentMapper.toDomain(likeCommentPer.comment); + const user = await UserMapper.toDomain(likeCommentPer.user); + return new LikeComment( likeCommentPer.id, likeCommentPer.commentId, - CommentMapper.toDomain(likeCommentPer.comment), + comment, likeCommentPer.userId, - UserMapper.toDomain(likeCommentPer.user), + user, likeCommentPer.createdAt, likeCommentPer.userId, likeCommentPer.updatedAt, diff --git a/server/src/infrastructure/like-posts/like-post.mapper.ts b/server/src/infrastructure/like-posts/like-post.mapper.ts index 125ce61a..b4473420 100644 --- a/server/src/infrastructure/like-posts/like-post.mapper.ts +++ b/server/src/infrastructure/like-posts/like-post.mapper.ts @@ -4,13 +4,15 @@ import { PostMapper } from '@infrastructure/posts/post.mapper'; import { LikePostPersistence } from './like-post.persistence'; class LikePostMapper { - static toDomain(likePostPer: LikePostPersistence): LikePost { + static async toDomain(likePostPer: LikePostPersistence): Promise { + const post = await PostMapper.toDomain(likePostPer.post); + const user = await UserMapper.toDomain(likePostPer.user); return new LikePost( likePostPer.id, likePostPer.postId, - PostMapper.toDomain(likePostPer.post), + post, likePostPer.userId, - UserMapper.toDomain(likePostPer.user), + user, likePostPer.createdAt, likePostPer.userId, likePostPer.updatedAt, @@ -20,17 +22,10 @@ class LikePostMapper { ); } - static toPersistence(likePost: LikePost): LikePostPersistence { - return { - id: likePost.id!, - createdAt: likePost.createdAt, - updatedAt: likePost.updatedAt!, - deletedAt: likePost.deletedAt!, - user: UserMapper.toPersistence(likePost.user), - post: PostMapper.toPersistence(likePost.post), - postId: likePost.postId, - userId: likePost.userId, - } + static toPersistence(likePost: LikePost): Promise { + const likePostPersistence = new LikePostPersistence(); + + return Promise.resolve(likePostPersistence); } } diff --git a/server/src/infrastructure/like-replies/like-reply.mapper.ts b/server/src/infrastructure/like-replies/like-reply.mapper.ts index 20944bee..7430080d 100644 --- a/server/src/infrastructure/like-replies/like-reply.mapper.ts +++ b/server/src/infrastructure/like-replies/like-reply.mapper.ts @@ -1,38 +1,50 @@ import { UserMapper } from '@infrastructure/users'; -import { LikeReply } from '@domain/entities'; +import { LikeReply, Reply, User } from '@domain/entities'; import { LikeReplyPersistence } from './like-reply.persistence'; import { ReplyMapper } from '@infrastructure/replies/reply.mapper'; class LikeReplyMapper { - static toDomain(likeReplyPer: LikeReplyPersistence): LikeReply { + static async toDomain( + likeReplyPer: LikeReplyPersistence, + ): Promise { + const userPromise = likeReplyPer.user + ? UserMapper.toDomain(likeReplyPer.user) + : null; + const replyPromise = likeReplyPer.reply + ? await ReplyMapper.toDomain(await likeReplyPer.reply) + : null; + + const [user, reply] = await Promise.all([userPromise, replyPromise]); + return { id: likeReplyPer.id, createdAt: likeReplyPer.createdAt, updatedAt: likeReplyPer.updatedAt, deletedAt: likeReplyPer.deletedAt, - user: UserMapper.toDomain(likeReplyPer.user), - reply: ReplyMapper.toDomain(likeReplyPer.reply), + user: user as User, + reply: reply as Reply, replyId: likeReplyPer.replyId, userId: likeReplyPer.userId, createdBy: likeReplyPer.userId, updatedBy: likeReplyPer.userId, deletedBy: likeReplyPer.userId, equals: (likeReply: LikeReply) => likeReply.id === likeReplyPer.id, - // Add the other two missing properties here - } + }; } - static toPersistence(likeReply: LikeReply): LikeReplyPersistence { + static async toPersistence( + likeReply: LikeReply, + ): Promise { return { id: likeReply.id!, createdAt: likeReply.createdAt, updatedAt: likeReply.updatedAt!, deletedAt: likeReply.deletedAt, - user: UserMapper.toPersistence(likeReply.user), + user: await UserMapper.toPersistence(likeReply.user), reply: ReplyMapper.toPersistence(likeReply.reply), replyId: likeReply.replyId, userId: likeReply.userId, - } + }; } } diff --git a/server/src/infrastructure/posts/post.mapper.ts b/server/src/infrastructure/posts/post.mapper.ts index fa98c529..67df0832 100644 --- a/server/src/infrastructure/posts/post.mapper.ts +++ b/server/src/infrastructure/posts/post.mapper.ts @@ -1,26 +1,61 @@ -import { Comment, LikePost, Post } from '@domain/entities'; +import { PostPersistence } from '@infrastructure/posts'; import { POST_STATUS } from '@domain/eums/post-status.enum'; -import { CommentMapper } from '@infrastructure/comments/comment.mapper'; -import { LikePostMapper } from '@infrastructure/like-posts/like-post.mapper'; -import { PostPersistence } from '@infrastructure/posts/post.persistence'; +import { Post } from '@domain/entities'; import { UserMapper } from '@infrastructure/users'; +import { CommentMapper } from '@infrastructure/comments'; +import { LikePostMapper } from '@infrastructure/like-posts'; +import { log } from 'console'; class PostMapper { - public static toDomain(postPersistence: PostPersistence): Post { - const { id, title, content, authorId, author, comments, likes, createdAt, deletedAt, updatedAt, status } = postPersistence; - - return new Post( - id, - title, content, authorId, UserMapper.toDomain(author), - likes.map(like => LikePostMapper.toDomain(like)), - comments.map(comment => CommentMapper.toDomain(comment)), - status as POST_STATUS, - createdAt, updatedAt, deletedAt + public static async toDomain(persistence: PostPersistence): Promise { + log(`Post Persistence after loading : `, await persistence.likes); + const likes = await persistence.likes; + const comments = await persistence.comments; + + return Post.create( + persistence.id, + persistence.title, + persistence.content, + persistence.authorId, + await UserMapper.toDomain(persistence.author), + await Promise.all(likes.map(like => LikePostMapper.toDomain(like))), + await Promise.all( + comments.map(comment => CommentMapper.toDomain(comment)), + ), + POST_STATUS[persistence.status.toUpperCase() as keyof typeof POST_STATUS], + persistence.createdAt, + persistence.updatedAt, + persistence.deletedAt, ); } - public static toPersistence(post: Post): PostPersistence { - return new PostPersistence(); + public static async toPersistence(domain: Post): Promise { + const postPersistence = new PostPersistence(); + if (domain.id) { + postPersistence.id = domain.id; + } + + postPersistence.title = domain.title; + postPersistence.content = domain.content; + postPersistence.authorId = domain.authorId; + postPersistence.author = await UserMapper.toPersistence(domain.author); + postPersistence.likes = Promise.resolve( + await Promise.all( + domain.likes.map(like => LikePostMapper.toPersistence(like)), + ), + ); + + postPersistence.comments = Promise.resolve( + await Promise.all( + domain.comments.map(comment => CommentMapper.toPersistence(comment)), + ), + ); + + postPersistence.status = domain.status.toLowerCase(); + postPersistence.createdAt = domain.createdAt; + postPersistence.deletedAt = domain.deletedAt; + + return postPersistence; } } diff --git a/server/src/infrastructure/posts/post.repository.impl.ts b/server/src/infrastructure/posts/post.repository.impl.ts index 27d61972..d01989c2 100644 --- a/server/src/infrastructure/posts/post.repository.impl.ts +++ b/server/src/infrastructure/posts/post.repository.impl.ts @@ -1,16 +1,12 @@ -import { - DataSource, - DeleteResult, - FindOptionsWhere, - Repository, - UpdateResult, -} from 'typeorm'; +import { DataSource, DeleteResult, Repository, UpdateResult } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { injectable } from 'inversify'; import { Post } from '@domain/entities'; import { IPostRepository } from '@domain/repositories/post.repository'; import { PostPersistence } from './post.persistence'; import { PostMapper } from './post.mapper'; +import { LOGGER } from '@/web/rest/logger'; +import { log } from 'console'; @injectable() export class PostRepository implements IPostRepository { @@ -25,7 +21,7 @@ export class PostRepository implements IPostRepository { } async createPost(post: Post): Promise { - const postPersistence = PostMapper.toPersistence(post); + const postPersistence = await PostMapper.toPersistence(post); const createdPost = await this._repository.save(postPersistence); return PostMapper.toDomain(createdPost); } @@ -45,15 +41,15 @@ export class PostRepository implements IPostRepository { take: limit, skip: (page - 1) * limit, }); - return posts.map(PostMapper.toDomain); + + const postPromises = posts.map(post => PostMapper.toDomain(post)); + return Promise.all(postPromises); } async findAll(limit: number, page: number): Promise { - const [posts, count] = await this._repository.findAndCount({ - take: limit, - skip: (page - 1) * limit, - }); - return posts.map(PostMapper.toDomain); + const [posts, count] = await this._repository.findAndCount(); + const postPromises = posts.map(post => PostMapper.toDomain(post)); + return Promise.all(postPromises); } async findByTitle(title: string): Promise { diff --git a/server/src/infrastructure/replies/reply.mapper.ts b/server/src/infrastructure/replies/reply.mapper.ts index e5db2ade..86cf5a65 100644 --- a/server/src/infrastructure/replies/reply.mapper.ts +++ b/server/src/infrastructure/replies/reply.mapper.ts @@ -5,15 +5,21 @@ import { LikeReplyMapper } from '@infrastructure/like-replies/like-reply.mapper' import { UserMapper } from '..'; class ReplyMapper { - static toDomain(reply: ReplyPersistence): Reply { + static async toDomain(reply: ReplyPersistence): Promise { + const comment = await CommentMapper.toDomain(reply.comment); + const user = await UserMapper.toDomain(reply.user); + const likes = await Promise.all( + (reply.likes ?? []).map(like => LikeReplyMapper.toDomain(like)), + ); + return new Reply( reply.id, reply.commentId, - CommentMapper.toDomain(reply.comment), + comment, reply.userId, - UserMapper.toDomain(reply.user), + user, reply.content, - reply.likes.map(like => LikeReplyMapper.toDomain(like)), + likes, reply.createdAt, reply.userId, reply.updatedAt, @@ -23,8 +29,23 @@ class ReplyMapper { ); } - static toPersistence(reply: Reply): ReplyPersistence { - return new ReplyPersistence(); + static async toPersistence(reply: Reply): Promise { + const replyPersistence = new ReplyPersistence(); + if (reply.id != null) { + replyPersistence.id = reply.id; + } + replyPersistence.commentId = reply.commentId; + replyPersistence.userId = reply.userId; + replyPersistence.content = reply.content; + replyPersistence.createdAt = reply.createdAt; + replyPersistence.deletedAt = reply.deletedAt; + replyPersistence.comment = await CommentMapper.toPersistence(reply.comment); + replyPersistence.user = await UserMapper.toPersistence(reply.user); + replyPersistence.likes = await Promise.all( + (reply.likes ?? []).map(like => LikeReplyMapper.toPersistence(like)), + ); + + return replyPersistence; } } diff --git a/server/src/infrastructure/users/user.mapper.ts b/server/src/infrastructure/users/user.mapper.ts index 52f655bb..3a356648 100644 --- a/server/src/infrastructure/users/user.mapper.ts +++ b/server/src/infrastructure/users/user.mapper.ts @@ -1,99 +1,91 @@ import { User } from '@domain/entities/user'; -import { UserPersistence } from './user.persistence'; -import { injectable } from 'inversify'; -import { - CommentMapper, - CommentPersistence, - LikeCommentMapper, - LikeReplyMapper, - PostMapper, - ReplyMapper, -} from '..'; +import { UserPersistence } from '@infrastructure/users/user.persistence'; import { LikePostMapper } from '@infrastructure/like-posts/like-post.mapper'; -@injectable() +import { LikeCommentMapper } from '@infrastructure/like-comments/like-comment.mapper'; +import { LikeReplyMapper } from '@infrastructure/like-replies/like-reply.mapper'; +import { CommentMapper } from '@infrastructure/comments/comment.mapper'; +import { PostMapper } from '@infrastructure/posts/post.mapper'; +import { ReplyMapper } from '@infrastructure/replies/reply.mapper'; + class UserMapper { - public static toDomain(userPersistenceModel: UserPersistence): User { - const { id } = userPersistenceModel; - return { - id: userPersistenceModel.id, - firstName: userPersistenceModel.firstName, - lastName: userPersistenceModel.lastName, - email: userPersistenceModel.email, - username: userPersistenceModel.username, - password: userPersistenceModel.hashedPassword, - createdAt: userPersistenceModel.createdAt, - updatedAt: userPersistenceModel.updatedAt, - deletedAt: userPersistenceModel.deletedAt, - comments: userPersistenceModel.comments?.map(comment => - CommentMapper.toDomain(comment), - ), - likedPosts: userPersistenceModel.likedPosts?.map(like => { - return LikePostMapper.toDomain(like); - }), - likedComments: userPersistenceModel.likedComments?.map(like => - LikeCommentMapper.toDomain(like), - ), - equals: (user: User) => user.id === id, - role: userPersistenceModel.role, - createdBy: userPersistenceModel.id as string, - updatedBy: userPersistenceModel.id, - deletedBy: userPersistenceModel.id, - isPasswordMatched: (password: string) => - userPersistenceModel.hashedPassword === password, - posts: userPersistenceModel.posts?.map(post => PostMapper.toDomain(post)), - replies: userPersistenceModel.replies?.map(reply => - ReplyMapper.toDomain(reply), - ), - likedReplies: userPersistenceModel.likedReplies?.map(like => - LikeReplyMapper.toDomain(like), - ), - }; + static async toDomain(persistence: UserPersistence): Promise { + const comments = await (persistence.comments ?? Promise.resolve([])); + const posts = await (persistence.posts ?? Promise.resolve([])); + const likedPosts = await (persistence.likedPosts ?? Promise.resolve([])); + const likedReplies = await (persistence.likedReplies ?? Promise.resolve([])); + const replies = await (persistence.replies ?? Promise.resolve([])); + const likedComments = await (persistence.likedComments ?? Promise.resolve([])); + + return User.create( + persistence.id, + persistence.firstName, + persistence.lastName, + persistence.email, + persistence.username, + persistence.hashedPassword, + persistence.role, + persistence.createdAt, + persistence.id ?? '', + persistence.updatedAt, + persistence.id ?? '', + await Promise.all(comments.map(comment => CommentMapper.toDomain(comment))), + await Promise.all(likedComments.map(likeComment => LikeCommentMapper.toDomain(likeComment))), + await Promise.all(posts.map(post => PostMapper.toDomain(post)),), + await Promise.all(likedPosts.map(likePost => LikePostMapper.toDomain(likePost))), + await Promise.all(replies.map(reply => ReplyMapper.toDomain(reply))), + await Promise.all(likedReplies.map(likeReply => LikeReplyMapper.toDomain(likeReply))), + ); } - public static toPersistence(user: User): UserPersistence { + static async toPersistence(domain: User): Promise { const userPersistence = new UserPersistence(); - if (user.id != null) { - userPersistence.id = user.id; - } - - userPersistence.firstName = user.firstName; - - userPersistence.lastName = user.lastName; - - userPersistence.email = user.email; - - userPersistence.username = user.username; - - userPersistence.hashedPassword = user.password; - - userPersistence.role = user.role; - - userPersistence.createdAt = user.createdAt; - - userPersistence.updatedAt = user.updatedAt; - - userPersistence.deletedAt = user.deletedAt; - - userPersistence.likedReplies = - user.likedReplies?.map(LikeReplyMapper.toPersistence) ?? []; - - userPersistence.likedReplies = - user.likedReplies?.map(LikeReplyMapper.toPersistence) ?? []; - - userPersistence.comments = - user.comments?.map(CommentMapper.toPersistence) ?? []; - - userPersistence.likedPosts = - user.likedPosts?.map(LikePostMapper.toPersistence) ?? []; - - userPersistence.likedComments = - user.likedComments?.map(LikeCommentMapper.toPersistence) ?? []; - - userPersistence.posts = user.posts?.map(PostMapper.toPersistence) ?? []; - - userPersistence.replies = - user.replies?.map(ReplyMapper.toPersistence) ?? []; + userPersistence.id = domain.id; + userPersistence.username = domain.username; + userPersistence.email = domain.email; + userPersistence.firstName = domain.firstName; + userPersistence.lastName = domain.lastName; + userPersistence.hashedPassword = domain.password; + userPersistence.role = domain.role; + userPersistence.createdAt = domain.createdAt; + userPersistence.updatedAt = domain.updatedAt; + userPersistence.deletedAt = domain.deletedAt; + + userPersistence.likedPosts = Promise.resolve( + await Promise.all( + domain.likedPosts?.map(likePost => LikePostMapper.toPersistence(likePost)) ?? [] + ) + ); + + userPersistence.replies = Promise.resolve( + await Promise.all( + domain.replies?.map(reply => ReplyMapper.toPersistence(reply)) ?? [] + ) + ); + + userPersistence.comments = Promise.resolve( + await Promise.all( + domain.comments?.map(comment => CommentMapper.toPersistence(comment)) ?? [] + ) + ); + + userPersistence.posts = Promise.resolve( + await Promise.all( + domain.posts?.map(post => PostMapper.toPersistence(post)) ?? [] + ) + ); + + userPersistence.likedComments = Promise.resolve( + await Promise.all( + domain.likedComments?.map(likeComment => LikeCommentMapper.toPersistence(likeComment)) ?? [] + ) + ); + + userPersistence.likedReplies = Promise.resolve( + await Promise.all( + domain.likedReplies?.map(likeReply => LikeReplyMapper.toPersistence(likeReply)) ?? [] + ) + ); return userPersistence; } diff --git a/server/src/infrastructure/users/user.repository.impl.ts b/server/src/infrastructure/users/user.repository.impl.ts index 9df02532..3974b894 100644 --- a/server/src/infrastructure/users/user.repository.impl.ts +++ b/server/src/infrastructure/users/user.repository.impl.ts @@ -43,14 +43,13 @@ class UserRepository implements IUserRepository { } async saveUser(user: User): Promise { - const persistence = UserMapper.toPersistence(user); - // Use repository.save directly for inserts + const persistence = await UserMapper.toPersistence(user); const savedUser = await this._repository.save(persistence); return UserMapper.toDomain(savedUser); } async updateUser(user: User): Promise { - const persistence = UserMapper.toPersistence(user); + const persistence = await UserMapper.toPersistence(user); const updatedUser = await this._repository.save(persistence); return UserMapper.toDomain(updatedUser); } From 7cfac23509766aa139aa2c8b3008dbf49a84fde0 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Thu, 30 May 2024 15:27:58 +0300 Subject: [PATCH 24/26] Adding find all posts use-case to inversify types and adding the proposed router --- server/db.sqlite3 | Bin 126976 -> 118784 bytes server/src/infrastructure/shared/ioc/types.ts | 1 + server/src/web/process.log | 6 ++--- .../web/rest/controllers/posts.controller.ts | 21 ++++++++++++++++++ server/src/web/rest/routes.ts | 3 ++- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/server/db.sqlite3 b/server/db.sqlite3 index 324447922b100544d5f02a617956a1d8ac292634..5d06b7bad1df455a953d62bbe79ba24809825e5d 100644 GIT binary patch delta 1521 zcmc(fU1%It6vyYx&dz>Jc5gOGoo&<5u}w6Vo$c&=>_=LX6Lj(!4JG03) z*=({q2}u>w=!!2^8>Tq54`NeeV=)FBVMP$>gMuIk^}#3#zF5Ti39Sef^zOzO)ChfZ zm~-dMJ$LTC=Xd^hPJiV%{j%e1qi-)k5KY+Xv60w9^8qQemk1UX?D!4P9o!Pz&>cj2 zPI@1ydC()dE-=5*U%L_%N1p@L_8D-%&0joA#_P=E?t@f)X%bm&%OG0cT?ABqd>Let z?y9BP0m&#-@E_;ikZtg%ukrz;52_>KA;sR&35mp#!bcR zr(APcyZK(tZu2&S%%5uBrWJZ~fZ_FnWA%)80>}n*wU#+{iQ(ESw#sm~*|8*%Wz6yRII~pulgm#8?Pmxq* zm8uK_bl*;z9qOn`pxe-MC?;7mdMjy->^+4Sg)F4~G6b*5saocPcYJnc41O zc^*o$CgW-`pP6ZlX0j z_9J9{-wX9t?eoyUkRCh{pag*uIMj(|=LB#iG1B|{lB}-8GP)QQ`KX#!cv;ajJ}S#P z{wWBG9+Sd?E^8afNdhN?+hnOtiUdQlv_~sarswjUp-&rJ lCSTxkGYpr>7K#R!8qE(`Um$3qxfzj^xI%ondGVN~B_?k`-H0 z9J^Q)yDE_W2%1(vx&0VWWEfg(>k8BvjAHLs2kgfl7zQlGvJJ?OwZqb8K=wepb-Sdj zhZA=Th9MF}-TU!e^1J7r`<-(yzd~OAJ$bFe*@e`rYC0!f`iYK+p0JZP%;GaZqv_-o+2_n|!jVUMT@_0Zos)=E6 zRJ+9hxRnZnJ%H>D?lHS&Jlda!K%4fX5zuXQZUiCek|a>=VTuR&CCf1W=DQ!Ot8cWygZy%r_w*N^Vz>9u-6Q?>0tGpOJWK$Y7sc}->j2QfH^)=q-jU;%t@%k*7v6KXeJ07uZ{9AMA3X_sz# z4}1yF7?$H%FU!+jfo8cbuQ$N>0>W<4$^3$xPRmM2 zUR;n@Rt*N6Hx~2xPZ(`?Jd^HbW8I9mJD=yys~&DPm*i*T%z`vNb?zLOpDoNv`KK3V z&!(l>T$D`-@yyxfT;!Cp5W1LLVMf+PGc{wdYd>nR^CuX;B*z&#A;>YBmn4Sv%c@8# zep!}yFUR_Q?0wvY059zVmAM!0BOi-#4^WRJlqa$m#upcY+*0<`?AbYfB$$kjE9^*Q zaQ4j1lK*U;Rfo@|r?VpR#>g4zq;es@E;Lk__ZZAoh2sT|q~>;d9lDLg%_Op0eF;}-|>;nYNI#hZ0Uhxxhb zi}7>AqvLBrK{`E^9vx*eD<@Zz6H8C$efjmHCWE=%?kYz_ToB?ufs^Tk&(G4l;`1Tq zeuYL+;`1tUJdT!pCrgeEc)hzoIW?Q(jLQi{O=nPe6e{tt93BZxxo3QXLzAi~pH#zV zf??I2FlL&HNu({sMGhZ8-k>mGudIer|gQWX;-D@Yte^Ri!{ zdA}H=WfYFIXL}%61Y9%^=tfZ5R zEZ3xazTCFxFVdTZ#YWv?J1LO5MY(ORHrWQRvv&{RYR#3g5EZ+f%Ym`8(&D<^miGPs99PaL=Q-%SnfnBlhTu9 zlfEVhsPrxRnxs;dZsQ)J7wk-{q?0&`pt@{j)*o7*1sc$(LfN}j9Wd!n+Nowl1!AgJ z_#UFqa^Lo|TUIbTcprmdxJ6y9m7-*;`6s5Bx!-7(xI&!53)EoxcRPG#qqKIsN@Cco7XGW`o*u)S(a*m_Ku@kJZB2z%keia1A%Jn#cKaHD%$99(E zCTsCxpXLbzH!4}53WG5mA9&2DvrVpNS9>)wg2q1?M)mwRM!{o1yEKdr7oUxSXHB+u z@xHoMLMIyh>*YQ=XxQ)pZ}pr}K8;x0k%gpJ_E!#SzX^gKgI0y@741?89M?udKzwj0 z(K_I%dzx{OX&G==p~F@u5&@%h4|f~q7E^OEe?aS)e?te&B{{|_EBx(Qv diff --git a/server/src/infrastructure/shared/ioc/types.ts b/server/src/infrastructure/shared/ioc/types.ts index c89cfb01..c62513ca 100644 --- a/server/src/infrastructure/shared/ioc/types.ts +++ b/server/src/infrastructure/shared/ioc/types.ts @@ -6,6 +6,7 @@ let TYPES = { IRegisterInputPort: Symbol.for('IRegisterInputPort'), ICreatePostInputPort: Symbol.for('ICreatePostInputPort'), + IFindAllPostInputPort: Symbol.for('IFindAllPostInputPort'), ICreatePostOutputPort: Symbol.for('ICreatePostOutputPort'), ILoginOutputPort: Symbol.for('ILoginOutputPort'), diff --git a/server/src/web/process.log b/server/src/web/process.log index e4c8bce2..00c1cd7a 100644 --- a/server/src/web/process.log +++ b/server/src/web/process.log @@ -1,3 +1,3 @@ -{"level":30,"time":1716762101785,"msg":"Application is running on port 2105 🚀"} -{"level":30,"time":1716762101792,"msg":"Environment: development"} -{"level":30,"time":1716762101813,"msg":"Data source initialized"} +{"level":30,"time":1717071773671,"msg":"Application is running on port 2105 🚀"} +{"level":30,"time":1717071773674,"msg":"Environment: development"} +{"level":30,"time":1717071773694,"msg":"Data source initialized"} diff --git a/server/src/web/rest/controllers/posts.controller.ts b/server/src/web/rest/controllers/posts.controller.ts index df2e68ba..3246296e 100644 --- a/server/src/web/rest/controllers/posts.controller.ts +++ b/server/src/web/rest/controllers/posts.controller.ts @@ -8,6 +8,8 @@ import { ExpressHandler } from '../infrastucture/express-handler'; import BaseController from './base.controller'; import { LOGGER } from '../logger'; import { POST_STATUS } from '@domain/eums/post-status.enum'; +import { FindAllPostRequest } from '@application/post/find-all/find-all-post.request'; +import { PostResponseDto } from '@contracts/dtos/posts'; @injectable() export class PostsController implements BaseController { @@ -15,14 +17,21 @@ export class PostsController implements BaseController { CreatePostRequest, CreatePostResponseDto >; + private _findAllPostUseCase: BaseUseCase< + FindAllPostRequest, + PostResponseDto[] + >; constructor( @inject(TYPES.ICreatePostInputPort) _createPostInteractor: BaseUseCase< CreatePostRequest, CreatePostResponseDto >, + @inject(TYPES.IFindAllPostInputPort) + _findAllPostInteractor: BaseUseCase, ) { this._createPostUseCase = _createPostInteractor; + this._findAllPostUseCase = _findAllPostInteractor; } public create: ExpressHandler = @@ -45,4 +54,16 @@ export class PostsController implements BaseController { const result = await this._createPostUseCase.execute(request); return res.json(result); }; + + public findAll: ExpressHandler = + async (req, res) => { + LOGGER.info('PostsController.findAll'); + const request = FindAllPostRequest.create( + new TriggeredByUser(res.locals.userId, []), + req.query.pageSize || 10, + req.query.pageNumber || 1, + ); + const result = await this._findAllPostUseCase.execute(request); + return result; + }; } diff --git a/server/src/web/rest/routes.ts b/server/src/web/rest/routes.ts index 28708f73..14fb7f39 100644 --- a/server/src/web/rest/routes.ts +++ b/server/src/web/rest/routes.ts @@ -27,6 +27,7 @@ export default class ApplicationRouter { app.post('/auth/login', this.getController(this.authController, 'login')); app .use(AuthMiddleware.jwtParseMiddleware) - .post('/posts', this.getController(this.postsController, 'create')); + .post('/posts', this.getController(this.postsController, 'create')) + .get('/posts', this.getController(this.postsController, 'findAll')); } } From ed44ee084097fe45f90caab7c51bae85d1986774 Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Fri, 31 May 2024 14:09:22 +0300 Subject: [PATCH 25/26] Circular dependency between mappers problem has been solved -need to be optimized- --- server/db.sqlite3 | Bin 118784 -> 118784 bytes .../post/create/create-post.usecase.ts | 9 +- .../post/find-all/find-all-post.usecase.ts | 2 + .../dtos/posts/post-details.response.ts | 2 + .../src/contracts/dtos/posts/post.response.ts | 6 +- .../src/contracts/dtos/users/user.response.ts | 2 +- .../src/infrastructure/posts/post.mapper.ts | 56 ++++--- .../infrastructure/posts/post.persistence.ts | 8 +- .../posts/post.repository.impl.ts | 7 +- .../shared/persistence/mapper.config.ts | 12 ++ .../shared/persistence/mapper.facade.ts | 97 ++++++++++++ .../src/infrastructure/users/user.mapper.ts | 139 ++++++++++++------ server/src/web/process.log | 6 +- .../web/rest/controllers/posts.controller.ts | 32 ++-- 14 files changed, 288 insertions(+), 90 deletions(-) create mode 100644 server/src/infrastructure/shared/persistence/mapper.config.ts create mode 100644 server/src/infrastructure/shared/persistence/mapper.facade.ts diff --git a/server/db.sqlite3 b/server/db.sqlite3 index 5d06b7bad1df455a953d62bbe79ba24809825e5d..0c8ecbb11563332bd01fee54af71f015219221d1 100644 GIT binary patch literal 118784 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 delta 142 zcmZozz}~QceS$Qv76Su=ED#F=G1Ej1b4IPkgslmTOZYdl2>jun7{JcT2$T`pEGTfE zUsWT?)YL55+$=@c(9+aE*Tg6}SvN7sz*slcB-zL!&A`CO*wB2L0b>IfFHjRBzb^y7 lFHom5|8!q@#?4$@V8I&<{5ODt=lG|u?`LG4e)l}1G63a@A=dx^ diff --git a/server/src/application/post/create/create-post.usecase.ts b/server/src/application/post/create/create-post.usecase.ts index f8e265a1..9820780e 100644 --- a/server/src/application/post/create/create-post.usecase.ts +++ b/server/src/application/post/create/create-post.usecase.ts @@ -1,10 +1,11 @@ import { BaseUseCase } from '@application/shared'; import { CreatePostRequest } from './create-post.request'; -import { PostDetailsResponseDto } from '@contracts/dtos/posts'; import { inject, injectable } from 'inversify'; import { TYPES } from '@infrastructure/shared/ioc/types'; import { IPostRepository } from '@domain/repositories/post.repository'; import { LOGGER } from '@/web/rest/logger'; +import { PostDetailsResponseDto } from '@contracts/dtos/posts/post-details.response'; +import { log } from 'console'; @injectable() class CreatePostUseCase extends BaseUseCase< @@ -20,8 +21,14 @@ class CreatePostUseCase extends BaseUseCase< public async performOperation( request: CreatePostRequest, ): Promise { + LOGGER.info('CreatePostUseCase.performOperation'); const post = CreatePostRequest.toEntity(request); + LOGGER.info( + 'mapped post is kjkhkhkhfkldhskjhdfksjahjdshajkhfdjskahfsdhjksah', + post.toString(), + ); const createdPost = await this._postRepository.createPost(post); + log('created post is', createdPost); if (createdPost) { return PostDetailsResponseDto.fromEntity(createdPost); } diff --git a/server/src/application/post/find-all/find-all-post.usecase.ts b/server/src/application/post/find-all/find-all-post.usecase.ts index af3a8c28..46035d22 100644 --- a/server/src/application/post/find-all/find-all-post.usecase.ts +++ b/server/src/application/post/find-all/find-all-post.usecase.ts @@ -4,6 +4,7 @@ import { PostResponseDto } from '@contracts/dtos/posts'; import { TYPES } from '@infrastructure/shared/ioc/types'; import { IPostRepository } from '@domain/repositories/post.repository'; import { inject } from 'inversify'; +import { log } from 'console'; class FindAllPostUseCase extends BaseUseCase< FindAllPostRequest, @@ -21,6 +22,7 @@ class FindAllPostUseCase extends BaseUseCase< ): Promise { const { pageSize, pageNumber } = request; const posts = await this._postRepository.findAll(pageSize, pageNumber); + log('posts from use-case', posts); const postDtos = await Promise.all( posts.map(async post => { return await PostResponseDto.fromEntity(post); diff --git a/server/src/contracts/dtos/posts/post-details.response.ts b/server/src/contracts/dtos/posts/post-details.response.ts index 08175ba4..a3a1f9bd 100644 --- a/server/src/contracts/dtos/posts/post-details.response.ts +++ b/server/src/contracts/dtos/posts/post-details.response.ts @@ -1,6 +1,8 @@ +import { LOGGER } from '@/web/rest/logger'; import { Post } from '@domain/entities'; import { Nullable } from '@domain/shared/types'; import { UserResponseDto } from '@dtos/users'; +import { log } from 'console'; export class PostDetailsResponseDto { constructor( diff --git a/server/src/contracts/dtos/posts/post.response.ts b/server/src/contracts/dtos/posts/post.response.ts index 2fb258d0..92a36c6c 100644 --- a/server/src/contracts/dtos/posts/post.response.ts +++ b/server/src/contracts/dtos/posts/post.response.ts @@ -1,7 +1,7 @@ import { Nullable } from '@domain/shared/types'; import { CommentResponseDto } from '@dtos/comments'; import { UserResponseDto } from '@dtos/users'; -import { Comment, Post } from '@domain/entities'; +import { Post } from '@domain/entities'; export class PostResponseDto { constructor( @@ -12,7 +12,7 @@ export class PostResponseDto { public comments: CommentResponseDto[], public createdAt: Date, public lastModifiedAt: Nullable, - ) { } + ) {} public static async fromEntity(entity: Post): Promise { return new PostResponseDto( @@ -20,7 +20,7 @@ export class PostResponseDto { entity.title, entity.content, UserResponseDto.fromEntity(entity.author), - entity.comments ? (await entity.comments).map(CommentResponseDto.fromEntity) : [], + entity.comments ? entity.comments.map(CommentResponseDto.fromEntity) : [], entity.createdAt, entity.updatedAt, ); diff --git a/server/src/contracts/dtos/users/user.response.ts b/server/src/contracts/dtos/users/user.response.ts index 902ae687..cf8a7212 100644 --- a/server/src/contracts/dtos/users/user.response.ts +++ b/server/src/contracts/dtos/users/user.response.ts @@ -8,7 +8,7 @@ export class UserResponseDto { public username: string, public email: string, public pictureProfile?: string, - ) { } + ) {} public static fromEntity(entity: User): UserResponseDto { return new UserResponseDto( diff --git a/server/src/infrastructure/posts/post.mapper.ts b/server/src/infrastructure/posts/post.mapper.ts index 67df0832..4e355c11 100644 --- a/server/src/infrastructure/posts/post.mapper.ts +++ b/server/src/infrastructure/posts/post.mapper.ts @@ -4,20 +4,29 @@ import { Post } from '@domain/entities'; import { UserMapper } from '@infrastructure/users'; import { CommentMapper } from '@infrastructure/comments'; import { LikePostMapper } from '@infrastructure/like-posts'; -import { log } from 'console'; +import { MapperConfig } from '@infrastructure/shared/persistence/mapper.config'; class PostMapper { - public static async toDomain(persistence: PostPersistence): Promise { - log(`Post Persistence after loading : `, await persistence.likes); - const likes = await persistence.likes; - const comments = await persistence.comments; + public static async toDomain( + persistence: PostPersistence, + config: MapperConfig = {}, + ): Promise { + const likes = config.includeLikes + ? await (persistence.likes ?? Promise.resolve([])) + : []; + const comments = config.includeComments + ? await (persistence.comments ?? Promise.resolve([])) + : []; + const author = config.includeAuthor + ? await UserMapper.toDomain(await persistence.author) + : null; return Post.create( persistence.id, persistence.title, persistence.content, persistence.authorId, - await UserMapper.toDomain(persistence.author), + author as any, await Promise.all(likes.map(like => LikePostMapper.toDomain(like))), await Promise.all( comments.map(comment => CommentMapper.toDomain(comment)), @@ -29,7 +38,10 @@ class PostMapper { ); } - public static async toPersistence(domain: Post): Promise { + public static async toPersistence( + domain: Post, + config: MapperConfig = {}, + ): Promise { const postPersistence = new PostPersistence(); if (domain.id) { postPersistence.id = domain.id; @@ -38,18 +50,26 @@ class PostMapper { postPersistence.title = domain.title; postPersistence.content = domain.content; postPersistence.authorId = domain.authorId; - postPersistence.author = await UserMapper.toPersistence(domain.author); - postPersistence.likes = Promise.resolve( - await Promise.all( - domain.likes.map(like => LikePostMapper.toPersistence(like)), - ), - ); - postPersistence.comments = Promise.resolve( - await Promise.all( - domain.comments.map(comment => CommentMapper.toPersistence(comment)), - ), - ); + if (config.includeAuthor) { + postPersistence.author = UserMapper.toPersistence(domain.author); + } + + if (config.includeLikes) { + postPersistence.likes = Promise.resolve( + await Promise.all( + domain.likes.map(like => LikePostMapper.toPersistence(like)), + ), + ); + } + + if (config.includeComments) { + postPersistence.comments = Promise.resolve( + await Promise.all( + domain.comments.map(comment => CommentMapper.toPersistence(comment)), + ), + ); + } postPersistence.status = domain.status.toLowerCase(); postPersistence.createdAt = domain.createdAt; diff --git a/server/src/infrastructure/posts/post.persistence.ts b/server/src/infrastructure/posts/post.persistence.ts index b1557887..8de0215d 100644 --- a/server/src/infrastructure/posts/post.persistence.ts +++ b/server/src/infrastructure/posts/post.persistence.ts @@ -7,6 +7,7 @@ import { UpdateDateColumn, CreateDateColumn, PrimaryGeneratedColumn, + JoinTable, } from 'typeorm'; import { CommentPersistence } from '@infrastructure/comments/'; import { UserPersistence } from '@infrastructure/users'; @@ -36,15 +37,18 @@ class PostPersistence { @Column() authorId: string; - @ManyToOne(() => UserPersistence, user => user.posts, { eager: true }) - author: UserPersistence; + @ManyToOne(() => UserPersistence, user => user.posts) + @JoinTable() + author: Promise; @OneToMany(() => CommentPersistence, comment => comment.post, { lazy: true }) + @JoinTable() comments: Promise; @OneToMany(() => LikePostPersistence, likePost => likePost.post, { lazy: true, }) + @JoinTable() likes: Promise; // add status column enum ['draft', 'published', 'deleted'] diff --git a/server/src/infrastructure/posts/post.repository.impl.ts b/server/src/infrastructure/posts/post.repository.impl.ts index d01989c2..204c6f5c 100644 --- a/server/src/infrastructure/posts/post.repository.impl.ts +++ b/server/src/infrastructure/posts/post.repository.impl.ts @@ -5,7 +5,6 @@ import { Post } from '@domain/entities'; import { IPostRepository } from '@domain/repositories/post.repository'; import { PostPersistence } from './post.persistence'; import { PostMapper } from './post.mapper'; -import { LOGGER } from '@/web/rest/logger'; import { log } from 'console'; @injectable() @@ -23,7 +22,7 @@ export class PostRepository implements IPostRepository { async createPost(post: Post): Promise { const postPersistence = await PostMapper.toPersistence(post); const createdPost = await this._repository.save(postPersistence); - return PostMapper.toDomain(createdPost); + return PostMapper.toDomain(createdPost, { includeAuthor: true }); } async findPostById(postId: string): Promise { @@ -48,7 +47,9 @@ export class PostRepository implements IPostRepository { async findAll(limit: number, page: number): Promise { const [posts, count] = await this._repository.findAndCount(); - const postPromises = posts.map(post => PostMapper.toDomain(post)); + const postPromises = posts.map(post => + PostMapper.toDomain(post, { includeAuthor: true }), + ); return Promise.all(postPromises); } diff --git a/server/src/infrastructure/shared/persistence/mapper.config.ts b/server/src/infrastructure/shared/persistence/mapper.config.ts new file mode 100644 index 00000000..438e38d3 --- /dev/null +++ b/server/src/infrastructure/shared/persistence/mapper.config.ts @@ -0,0 +1,12 @@ +interface MapperConfig { + includeComments?: boolean; + includePosts?: boolean; + includeLikedPosts?: boolean; + includeLikedReplies?: boolean; + includeReplies?: boolean; + includeLikedComments?: boolean; + includeLikes?: boolean; + includeAuthor?: boolean; +} + +export { MapperConfig }; diff --git a/server/src/infrastructure/shared/persistence/mapper.facade.ts b/server/src/infrastructure/shared/persistence/mapper.facade.ts new file mode 100644 index 00000000..98d73359 --- /dev/null +++ b/server/src/infrastructure/shared/persistence/mapper.facade.ts @@ -0,0 +1,97 @@ +import { LikeReply, Reply, User } from '@domain/entities/'; +import { Post } from '@domain/entities/'; +import { Comment } from '@domain/entities'; +import { LikePost } from '@domain/entities/'; +import { UserPersistence, UserMapper } from '@infrastructure/users/'; +import { PostPersistence, PostMapper } from '@infrastructure/posts/'; +import { CommentPersistence, CommentMapper } from '@infrastructure/comments/'; +import { + LikePostPersistence, + LikePostMapper, +} from '@infrastructure/like-posts/'; +import { ReplyMapper, ReplyPersistence } from '@infrastructure/replies'; +import { + LikeReplyMapper, + LikeReplyPersistence, +} from '@infrastructure/like-replies'; +import { + LikeCommentMapper, + LikeCommentPersistence, +} from '@infrastructure/like-comments'; +import { LikeComment } from '@domain/entities/like-comment'; + +class MapperFacade { + static async toUserDomain(persistence: UserPersistence): Promise { + return await UserMapper.toDomain(persistence); + } + + static async toUserPersistence(domain: User): Promise { + return await UserMapper.toPersistence(domain); + } + + static async toPostDomain(persistence: PostPersistence): Promise { + return await PostMapper.toDomain(persistence); + } + + static async toPostPersistence(domain: Post): Promise { + return await PostMapper.toPersistence(domain); + } + + static async toCommentDomain( + persistence: CommentPersistence, + ): Promise { + return await CommentMapper.toDomain(persistence); + } + + static async toCommentPersistence( + domain: Comment, + ): Promise { + return await CommentMapper.toPersistence(domain); + } + + static async toLikePostDomain( + persistence: LikePostPersistence, + ): Promise { + return await LikePostMapper.toDomain(persistence); + } + + static async toLikePostPersistence( + domain: LikePost, + ): Promise { + return await LikePostMapper.toPersistence(domain); + } + + static async toLikeCommentDomain( + persistence: LikeCommentPersistence, + ): Promise { + return await LikeCommentMapper.toDomain(persistence); + } + + static async toLikeCommentPersistence( + domain: LikeComment, + ): Promise { + return await LikeCommentMapper.toPersistence(domain); + } + + static async toLikeReplyDomain( + persistence: LikeReplyPersistence, + ): Promise { + return await LikeReplyMapper.toDomain(persistence); + } + + static async toLikeReplyPersistence( + domain: LikeReply, + ): Promise { + return await LikeReplyMapper.toPersistence(domain); + } + + static async toReplyDomain(persistence: ReplyPersistence): Promise { + return await ReplyMapper.toDomain(persistence); + } + + static async toReplyPersistence(domain: Reply): Promise { + return await ReplyMapper.toPersistence(domain); + } +} + +export { MapperFacade }; diff --git a/server/src/infrastructure/users/user.mapper.ts b/server/src/infrastructure/users/user.mapper.ts index 3a356648..8d7b25c0 100644 --- a/server/src/infrastructure/users/user.mapper.ts +++ b/server/src/infrastructure/users/user.mapper.ts @@ -6,15 +6,31 @@ import { LikeReplyMapper } from '@infrastructure/like-replies/like-reply.mapper' import { CommentMapper } from '@infrastructure/comments/comment.mapper'; import { PostMapper } from '@infrastructure/posts/post.mapper'; import { ReplyMapper } from '@infrastructure/replies/reply.mapper'; +import { MapperConfig } from '@infrastructure/shared/persistence/mapper.config'; class UserMapper { - static async toDomain(persistence: UserPersistence): Promise { - const comments = await (persistence.comments ?? Promise.resolve([])); - const posts = await (persistence.posts ?? Promise.resolve([])); - const likedPosts = await (persistence.likedPosts ?? Promise.resolve([])); - const likedReplies = await (persistence.likedReplies ?? Promise.resolve([])); - const replies = await (persistence.replies ?? Promise.resolve([])); - const likedComments = await (persistence.likedComments ?? Promise.resolve([])); + static async toDomain( + persistence: UserPersistence, + config: MapperConfig = {}, + ): Promise { + const comments = config.includeComments + ? await (persistence.comments ?? Promise.resolve([])) + : []; + const posts = config.includePosts + ? await (persistence.posts ?? Promise.resolve([])) + : []; + const likedPosts = config.includeLikedPosts + ? await (persistence.likedPosts ?? Promise.resolve([])) + : []; + const likedReplies = config.includeLikedReplies + ? await (persistence.likedReplies ?? Promise.resolve([])) + : []; + const replies = config.includeReplies + ? await (persistence.replies ?? Promise.resolve([])) + : []; + const likedComments = config.includeLikedComments + ? await (persistence.likedComments ?? Promise.resolve([])) + : []; return User.create( persistence.id, @@ -28,19 +44,34 @@ class UserMapper { persistence.id ?? '', persistence.updatedAt, persistence.id ?? '', - await Promise.all(comments.map(comment => CommentMapper.toDomain(comment))), - await Promise.all(likedComments.map(likeComment => LikeCommentMapper.toDomain(likeComment))), - await Promise.all(posts.map(post => PostMapper.toDomain(post)),), - await Promise.all(likedPosts.map(likePost => LikePostMapper.toDomain(likePost))), + await Promise.all( + comments.map(comment => CommentMapper.toDomain(comment)), + ), + await Promise.all( + likedComments.map(likeComment => + LikeCommentMapper.toDomain(likeComment), + ), + ), + await Promise.all(posts.map(post => PostMapper.toDomain(post))), + await Promise.all( + likedPosts.map(likePost => LikePostMapper.toDomain(likePost)), + ), await Promise.all(replies.map(reply => ReplyMapper.toDomain(reply))), - await Promise.all(likedReplies.map(likeReply => LikeReplyMapper.toDomain(likeReply))), + await Promise.all( + likedReplies.map(likeReply => LikeReplyMapper.toDomain(likeReply)), + ), ); } - static async toPersistence(domain: User): Promise { + static async toPersistence( + domain: User, + config: MapperConfig = {}, + ): Promise { const userPersistence = new UserPersistence(); - userPersistence.id = domain.id; + if (domain.id) { + userPersistence.id = domain.id; + } userPersistence.username = domain.username; userPersistence.email = domain.email; userPersistence.firstName = domain.firstName; @@ -51,41 +82,61 @@ class UserMapper { userPersistence.updatedAt = domain.updatedAt; userPersistence.deletedAt = domain.deletedAt; - userPersistence.likedPosts = Promise.resolve( - await Promise.all( - domain.likedPosts?.map(likePost => LikePostMapper.toPersistence(likePost)) ?? [] - ) - ); + if (config.includeLikedPosts) { + userPersistence.likedPosts = Promise.resolve( + await Promise.all( + domain.likedPosts?.map(likePost => + LikePostMapper.toPersistence(likePost), + ) ?? [], + ), + ); + } - userPersistence.replies = Promise.resolve( - await Promise.all( - domain.replies?.map(reply => ReplyMapper.toPersistence(reply)) ?? [] - ) - ); + if (config.includeReplies) { + userPersistence.replies = Promise.resolve( + await Promise.all( + domain.replies?.map(reply => ReplyMapper.toPersistence(reply)) ?? [], + ), + ); + } - userPersistence.comments = Promise.resolve( - await Promise.all( - domain.comments?.map(comment => CommentMapper.toPersistence(comment)) ?? [] - ) - ); + if (config.includeComments) { + userPersistence.comments = Promise.resolve( + await Promise.all( + domain.comments?.map(comment => + CommentMapper.toPersistence(comment), + ) ?? [], + ), + ); + } - userPersistence.posts = Promise.resolve( - await Promise.all( - domain.posts?.map(post => PostMapper.toPersistence(post)) ?? [] - ) - ); + if (config.includePosts) { + userPersistence.posts = Promise.resolve( + await Promise.all( + domain.posts?.map(post => PostMapper.toPersistence(post)) ?? [], + ), + ); + } - userPersistence.likedComments = Promise.resolve( - await Promise.all( - domain.likedComments?.map(likeComment => LikeCommentMapper.toPersistence(likeComment)) ?? [] - ) - ); + if (config.includeLikedComments) { + userPersistence.likedComments = Promise.resolve( + await Promise.all( + domain.likedComments?.map(likeComment => + LikeCommentMapper.toPersistence(likeComment), + ) ?? [], + ), + ); + } - userPersistence.likedReplies = Promise.resolve( - await Promise.all( - domain.likedReplies?.map(likeReply => LikeReplyMapper.toPersistence(likeReply)) ?? [] - ) - ); + if (config.includeLikedReplies) { + userPersistence.likedReplies = Promise.resolve( + await Promise.all( + domain.likedReplies?.map(likeReply => + LikeReplyMapper.toPersistence(likeReply), + ) ?? [], + ), + ); + } return userPersistence; } diff --git a/server/src/web/process.log b/server/src/web/process.log index 00c1cd7a..8ead4c8d 100644 --- a/server/src/web/process.log +++ b/server/src/web/process.log @@ -1,3 +1,3 @@ -{"level":30,"time":1717071773671,"msg":"Application is running on port 2105 🚀"} -{"level":30,"time":1717071773674,"msg":"Environment: development"} -{"level":30,"time":1717071773694,"msg":"Data source initialized"} +{"level":30,"time":1717153656237,"msg":"Application is running on port 2105 🚀"} +{"level":30,"time":1717153656240,"msg":"Environment: development"} +{"level":30,"time":1717153656261,"msg":"Data source initialized"} diff --git a/server/src/web/rest/controllers/posts.controller.ts b/server/src/web/rest/controllers/posts.controller.ts index 3246296e..ac78b2dc 100644 --- a/server/src/web/rest/controllers/posts.controller.ts +++ b/server/src/web/rest/controllers/posts.controller.ts @@ -4,12 +4,13 @@ import { CreatePostResponseDto } from '@contracts/dtos/posts/create/create-post. import { TriggeredByUser } from '@domain/shared/entities'; import { TYPES } from '@infrastructure/shared/ioc/types'; import { inject, injectable } from 'inversify'; -import { ExpressHandler } from '../infrastucture/express-handler'; import BaseController from './base.controller'; import { LOGGER } from '../logger'; import { POST_STATUS } from '@domain/eums/post-status.enum'; import { FindAllPostRequest } from '@application/post/find-all/find-all-post.request'; import { PostResponseDto } from '@contracts/dtos/posts'; +import { ExpressHandler } from '../infrastucture/express-handler'; +import { log } from 'console'; @injectable() export class PostsController implements BaseController { @@ -21,27 +22,24 @@ export class PostsController implements BaseController { FindAllPostRequest, PostResponseDto[] >; + constructor( @inject(TYPES.ICreatePostInputPort) - _createPostInteractor: BaseUseCase< - CreatePostRequest, - CreatePostResponseDto - >, + createPostInteractor: BaseUseCase, @inject(TYPES.IFindAllPostInputPort) - _findAllPostInteractor: BaseUseCase, + findAllPostInteractor: BaseUseCase, ) { - this._createPostUseCase = _createPostInteractor; - this._findAllPostUseCase = _findAllPostInteractor; + this._createPostUseCase = createPostInteractor; + this._findAllPostUseCase = findAllPostInteractor; } public create: ExpressHandler = - async (req, res) => { + async (req: any, res: any) => { LOGGER.info('PostsController.create'); const { title, content } = req.body; if (!title || !content) { return res.status(400).json({}); } - const request = CreatePostRequest.create( new TriggeredByUser(res.locals.userId, []), title!, @@ -51,19 +49,23 @@ export class PostsController implements BaseController { [], POST_STATUS.DRAFT, ); + log('request', request.toString()); const result = await this._createPostUseCase.execute(request); + log('result after creation', result); return res.json(result); }; - public findAll: ExpressHandler = - async (req, res) => { + async (req: any, res: any) => { LOGGER.info('PostsController.findAll'); + const pageSize = parseInt(req.query.pageSize as string) || 10; + const pageNumber = parseInt(req.query.pageNumber as string) || 1; + const request = FindAllPostRequest.create( new TriggeredByUser(res.locals.userId, []), - req.query.pageSize || 10, - req.query.pageNumber || 1, + pageSize, + pageNumber, ); const result = await this._findAllPostUseCase.execute(request); - return result; + return res.json(result); }; } From 674c1cc7ce1b4d41dbf4a15eb8b855e6e82ed23b Mon Sep 17 00:00:00 2001 From: Muhammad Banhawy Date: Fri, 31 May 2024 14:12:09 +0300 Subject: [PATCH 26/26] cleanup --- server/db.sqlite3 | Bin 118784 -> 118784 bytes server/src/web/process.log | 16 +++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/server/db.sqlite3 b/server/db.sqlite3 index 0c8ecbb11563332bd01fee54af71f015219221d1..d0d7565a416b00e0f84d91c181117085c8db651e 100644 GIT binary patch delta 220 zcmZozz}~QceS$Qj`$QRMM)$^qtqF`vGt}CcK|MHB+$hl?$-*SrMAz6j(M;FG%)&s|GR4qT*TTXw(bUk)BGD*0(a6Ba zMAyJn*Vs_O(9p`zz{9Mv)vt0Y-sfwO|3H9}))(8vqYY4w?<;4Kxga3(gB0vk?#l i3%4690qK+o9Sd9l4iyZfw+_GoB@2