diff --git a/src/modules/blogs/blogs.service.ts b/src/modules/blogs/blogs.service.ts index e7c8e33c9..9877fb050 100644 --- a/src/modules/blogs/blogs.service.ts +++ b/src/modules/blogs/blogs.service.ts @@ -12,10 +12,8 @@ import { User } from '@modules/user/entities/user.entity'; @Injectable() export class BlogService { constructor( - @InjectRepository(Blog) - private blogRepository: Repository, - @InjectRepository(User) - private userRepository: Repository + @InjectRepository(Blog) private blogRepository: Repository, + @InjectRepository(User) private userRepository: Repository ) {} private async fetchUserById(userId: string): Promise { @@ -27,31 +25,30 @@ export class BlogService { if (!user) { throw new CustomHttpException('User not found.', HttpStatus.NOT_FOUND); } - return user; } + private async findBlogById(id: string, relations: string[] = []): Promise { + const blog = await this.blogRepository.findOne({ where: { id }, relations }); + if (!blog) { + throw new CustomHttpException(SYS_MSG.BLOG_NOT_FOUND, HttpStatus.NOT_FOUND); + } + return blog; + } + async createBlog(createBlogDto: CreateBlogDto, user: User): Promise { const fullUser = await this.fetchUserById(user.id); - const blog = this.blogRepository.create({ - ...createBlogDto, - author: fullUser, - }); - + const blog = this.blogRepository.create({ ...createBlogDto, author: fullUser }); const savedBlog = await this.blogRepository.save(blog); - return { - blog_id: savedBlog.id, - title: savedBlog.title, - content: savedBlog.content, - tags: savedBlog.tags, - image_urls: savedBlog.image_urls, - author: `${fullUser.first_name} ${fullUser.last_name}`, - created_at: savedBlog.created_at, - }; + return this.formatBlogResponse(savedBlog); } + + async getSingleBlog(blogId: string): Promise { + const blog = await this.findBlogById(blogId, ['author']); + async getSingleBlog( blogId: string, user: User @@ -70,41 +67,40 @@ export class BlogService { const { id, created_at, updated_at, ...rest } = singleBlog; const author = `${fullName.first_name} ${fullName.last_name}`; + return { - status_code: 200, + status_code: HttpStatus.OK, message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + + data: this.formatBlogResponse(blog), + data: { blog_id: id, ...rest, author, published_date: created_at, created_at }, + }; } async updateBlog(id: string, updateBlogDto: UpdateBlogDto, user: User): Promise { - const blog = await this.blogRepository.findOne({ - where: { id }, - relations: ['author'], - }); - - if (!blog) { - throw new CustomHttpException('Blog post not found.', HttpStatus.NOT_FOUND); - } - + const blog = await this.findBlogById(id, ['author']); const fullUser = await this.fetchUserById(user.id); Object.assign(blog, updateBlogDto, { author: fullUser }); - const updatedBlog = await this.blogRepository.save(blog); - return { - blog_id: updatedBlog.id, - title: updatedBlog.title, - content: updatedBlog.content, - tags: updatedBlog.tags, - image_urls: updatedBlog.image_urls, - author: `${updatedBlog.author.first_name} ${updatedBlog.author.last_name}`, - created_at: updatedBlog.created_at, - }; + return this.formatBlogResponse(updatedBlog); } async deleteBlogPost(id: string): Promise { + + const blog = await this.findBlogById(id); + await this.blogRepository.remove(blog); + } + + async getAllBlogs(page: number, pageSize: number) { + const skip = (page - 1) * pageSize; + const [result, total] = await this.blogRepository.findAndCount({ skip, take: pageSize, relations: ['author'] }); + + return this.formatPaginatedResponse(result, total, page, pageSize, SYS_MSG.BLOG_FETCHED_SUCCESSFUL); + const blog = await this.blogRepository.findOne({ where: { id } }); if (!blog) { throw new CustomHttpException('Blog post with this id does not exist.', HttpStatus.NOT_FOUND); @@ -155,19 +151,15 @@ export class BlogService { blogs: result.data.blogs, }, }; + } - async searchBlogs(query: any): Promise<{ - status_code: number; - message: string; - data: { current_page: number; total_pages: number; total_results: number; blogs: BlogResponseDto[]; meta: any }; - }> { + async searchBlogs(query: any) { const { page = 1, page_size = 10 } = query; const skip = (page - 1) * page_size; this.validateEmptyValues(query); - - const where: FindOptionsWhere = this.buildWhereClause(query); + const where = this.buildWhereClause(query); const [result, total] = await this.blogRepository.findAndCount({ where: Object.keys(where).length ? where : undefined, @@ -176,41 +168,65 @@ export class BlogService { relations: ['author'], }); - if (!result || result.length === 0) { - return { - status_code: HttpStatus.NOT_FOUND, - message: 'no_results_found_for_the_provided_search_criteria', - data: { - current_page: page, - total_pages: 0, - total_results: 0, - blogs: [], - meta: { - has_next: false, - total: 0, - next_page: null, - prev_page: null, - }, - }, - }; + return this.formatPaginatedResponse( + result, + total, + page, + page_size, + result.length ? SYS_MSG.BLOG_FETCHED_SUCCESSFUL : 'No results found.' + ); + } + + private buildWhereClause(query: any): FindOptionsWhere { + const where: FindOptionsWhere = {}; + if (query.author) where.author = { first_name: Like(`%${query.author}%`), last_name: Like(`%${query.author}%`) }; + if (query.title) where.title = Like(`%${query.title}%`); + if (query.content) where.content = Like(`%${query.content}%`); + if (query.tags) where.tags = Like(`%${query.tags}%`); + if (query.created_date) where.created_at = MoreThanOrEqual(new Date(query.created_date)); + return where; + } + + private validateEmptyValues(query: any): void { + for (const key in query) { + if (query[key] !== undefined && typeof query[key] === 'string' && !query[key].trim()) { + throw new CustomHttpException(`${key.replace(/_/g, ' ')} value is empty`, HttpStatus.BAD_REQUEST); + } } + } - const data = this.mapBlogResults(result); - const totalPages = Math.ceil(total / page_size); + private formatBlogResponse(blog: Blog): BlogResponseDto { + return { + blog_id: blog.id, + title: blog.title, + content: blog.content, + tags: blog.tags, + image_urls: blog.image_urls, + author: blog.author ? `${blog.author.first_name} ${blog.author.last_name}` : 'Unknown', + created_at: blog.created_at, + }; + } + + private formatPaginatedResponse(result: Blog[], total: number, page: number, pageSize: number, message: string) { + const totalPages = Math.ceil(total / pageSize); return { - status_code: HttpStatus.OK, - message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + status_code: total > 0 ? HttpStatus.OK : HttpStatus.NOT_FOUND, + message, data: { current_page: page, total_pages: totalPages, total_results: total, + + blogs: result.map(blog => this.formatBlogResponse(blog)), + blogs: data .filter(blog => !blog.deletedAt) .map(blog => { const { deletedAt, ...rest } = blog; return rest; }), + meta: { has_next: page < totalPages, total, diff --git a/src/modules/blogs/tests/blogs.service.spec.ts b/src/modules/blogs/tests/blogs.service.spec.ts index 5505c564f..a973e5736 100644 --- a/src/modules/blogs/tests/blogs.service.spec.ts +++ b/src/modules/blogs/tests/blogs.service.spec.ts @@ -7,6 +7,10 @@ import { Repository, Like, MoreThanOrEqual, Not, IsNull } from 'typeorm'; import { Blog } from '../entities/blog.entity'; import { User } from '@modules/user/entities/user.entity'; import { BlogService } from '../blogs.service'; +import { CreateBlogDto } from '../dtos/create-blog.dto'; +import { UpdateBlogDto } from '../dtos/update-blog.dto'; +import { HttpStatus } from '@nestjs/common'; +import { CustomHttpException } from '@shared/helpers/custom-http-filter'; describe('BlogService', () => { let service: BlogService; @@ -21,7 +25,6 @@ describe('BlogService', () => { create: jest.fn(), save: jest.fn(), findAndCount: jest.fn(), - findOneBy: jest.fn(), findOne: jest.fn(), remove: jest.fn(), softRemove: jest.fn(), @@ -43,7 +46,7 @@ describe('BlogService', () => { describe('createBlog', () => { it('should successfully create a blog', async () => { - const createBlogDto = { + const createBlogDto: CreateBlogDto = { title: 'Test Blog', content: 'Test Content', tags: ['test'], @@ -55,19 +58,22 @@ describe('BlogService', () => { user.first_name = 'John'; user.last_name = 'Doe'; - const fullUser = { first_name: 'John', last_name: 'Doe' }; - const blog = new Blog(); blog.id = 'blog-id'; blog.title = 'Test Blog'; blog.content = 'Test Content'; blog.tags = ['test']; blog.image_urls = ['http://example.com/image.jpg']; - blog.author = fullUser as unknown as User; + blog.author = user; blog.created_at = new Date(); - blog.updated_at = new Date(); - const expectedResponse = { + jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); + jest.spyOn(blogRepository, 'create').mockReturnValue(blog); + jest.spyOn(blogRepository, 'save').mockResolvedValue(blog); + + const result = await service.createBlog(createBlogDto, user); + + expect(result).toEqual({ blog_id: 'blog-id', title: 'Test Blog', content: 'Test Content', @@ -75,28 +81,20 @@ describe('BlogService', () => { image_urls: ['http://example.com/image.jpg'], author: 'John Doe', created_at: blog.created_at, - }; - - jest.spyOn(userRepository, 'findOne').mockResolvedValue(fullUser as unknown as User); - jest.spyOn(blogRepository, 'create').mockReturnValue(blog); - jest.spyOn(blogRepository, 'save').mockResolvedValue(blog); - - const result = await service.createBlog(createBlogDto, user); - - expect(result).toEqual(expectedResponse); + }); expect(userRepository.findOne).toHaveBeenCalledWith({ - where: { id: user.id }, + where: { id: 'user-id' }, select: ['first_name', 'last_name'], }); expect(blogRepository.create).toHaveBeenCalledWith({ ...createBlogDto, - author: fullUser, + author: user, }); expect(blogRepository.save).toHaveBeenCalledWith(blog); }); it('should throw an error if user not found', async () => { - const createBlogDto = { + const createBlogDto: CreateBlogDto = { title: 'Test Blog', content: 'Test Content', tags: ['test'], @@ -108,7 +106,174 @@ describe('BlogService', () => { jest.spyOn(userRepository, 'findOne').mockResolvedValue(null); - await expect(service.createBlog(createBlogDto, user)).rejects.toThrow('User not found'); + await expect(service.createBlog(createBlogDto, user)).rejects.toThrow('User not found.'); + }); + }); + + describe('getSingleBlog', () => { + it('should successfully retrieve a blog', async () => { + const user = new User(); + user.id = 'user-id'; + user.first_name = 'John'; + user.last_name = 'Doe'; + + const blog = new Blog(); + blog.id = 'blog-id'; + blog.title = 'Test Blog'; + blog.content = 'Test Content'; + blog.tags = ['test']; + blog.image_urls = ['http://example.com/image.jpg']; + blog.author = user; + blog.created_at = new Date(); + + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(blog); + + const result = await service.getSingleBlog('blog-id'); + + expect(result).toEqual({ + status_code: HttpStatus.OK, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + blog_id: 'blog-id', + title: 'Test Blog', + content: 'Test Content', + tags: ['test'], + image_urls: ['http://example.com/image.jpg'], + author: 'John Doe', + created_at: blog.created_at, + }, + }); + expect(blogRepository.findOne).toHaveBeenCalledWith({ + where: { id: 'blog-id' }, + relations: ['author'], + }); + }); + + it('should throw an error if blog not found', async () => { + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(null); + + await expect(service.getSingleBlog('non-existent-blog-id')).rejects.toThrow(SYS_MSG.BLOG_NOT_FOUND); + }); + }); + + describe('updateBlog', () => { + it('should successfully update a blog', async () => { + const updateBlogDto: UpdateBlogDto = { + title: 'Updated Blog Title', + }; + + const user = new User(); + user.id = 'user-id'; + user.first_name = 'John'; + user.last_name = 'Doe'; + + const blog = new Blog(); + blog.id = 'blog-id'; + blog.title = 'Test Blog'; + blog.content = 'Test Content'; + blog.tags = ['test']; + blog.image_urls = ['http://example.com/image.jpg']; + blog.author = user; + blog.created_at = new Date(); + + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(blog); + jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); + jest.spyOn(blogRepository, 'save').mockResolvedValue(blog); + + const result = await service.updateBlog('blog-id', updateBlogDto, user); + + expect(result).toEqual({ + blog_id: 'blog-id', + title: 'Updated Blog Title', + content: 'Test Content', + tags: ['test'], + image_urls: ['http://example.com/image.jpg'], + author: 'John Doe', + created_at: blog.created_at, + }); + expect(blogRepository.findOne).toHaveBeenCalledWith({ + where: { id: 'blog-id' }, + relations: ['author'], + }); + expect(blogRepository.save).toHaveBeenCalledWith(blog); + }); + }); + + describe('deleteBlogPost', () => { + it('should successfully delete a blog post', async () => { + const blog = new Blog(); + blog.id = 'blog-id'; + + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(blog); + jest.spyOn(blogRepository, 'remove').mockResolvedValue(undefined); + + await service.deleteBlogPost('blog-id'); + + expect(blogRepository.findOne).toHaveBeenCalledWith({ + where: { id: 'blog-id' }, + relations: [], + }); + expect(blogRepository.remove).toHaveBeenCalledWith(blog); + }); + + it('should throw a 404 error if blog not found', async () => { + jest.spyOn(blogRepository, 'findOne').mockResolvedValue(null); + + await expect(service.deleteBlogPost('non-existent-blog-id')).rejects.toThrow(SYS_MSG.BLOG_NOT_FOUND); + }); + }); + + describe('getAllBlogs', () => { + it('should return paginated blog results', async () => { + const user = new User(); + user.id = 'user-id'; + user.first_name = 'John'; + user.last_name = 'Doe'; + + const blog = new Blog(); + blog.id = 'blog-id'; + blog.title = 'Test Blog'; + blog.content = 'Test Content'; + blog.tags = ['test']; + blog.image_urls = ['http://example.com/image.jpg']; + blog.author = user; + blog.created_at = new Date(); + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); + + const result = await service.getAllBlogs(1, 10); + + expect(result).toEqual({ + status_code: HttpStatus.OK, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + current_page: 1, + total_pages: 1, + total_results: 1, + blogs: [ + { + blog_id: 'blog-id', + title: 'Test Blog', + content: 'Test Content', + tags: ['test'], + image_urls: ['http://example.com/image.jpg'], + author: 'John Doe', + created_at: blog.created_at, + }, + ], + meta: { + has_next: false, + total: 1, + next_page: null, + prev_page: null, + }, + }, + }); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + skip: 0, + take: 10, + relations: ['author'], + }); }); }); @@ -125,6 +290,7 @@ describe('BlogService', () => { }; const user = new User(); + user.id = 'user-id'; user.first_name = 'John'; user.last_name = 'Doe'; @@ -136,10 +302,13 @@ describe('BlogService', () => { blog.image_urls = ['http://example.com/image.jpg']; blog.author = user; blog.created_at = new Date('2023-01-01'); - blog.updated_at = new Date(); - const expectedResponse = { - status_code: 200, + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); + + const result = await service.searchBlogs(query); + + expect(result).toEqual({ + status_code: HttpStatus.OK, message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, data: { current_page: 1, @@ -153,8 +322,12 @@ describe('BlogService', () => { tags: ['test'], image_urls: ['http://example.com/image.jpg'], author: 'John Doe', + + created_at: blog.created_at, + created_at: new Date('2023-01-01'), deletedAt: undefined, + }, ], meta: { @@ -164,19 +337,10 @@ describe('BlogService', () => { prev_page: null, }, }, - }; - - jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); - - const result = await service.searchBlogs(query); - - expect(result).toEqual(expectedResponse); + }); expect(blogRepository.findAndCount).toHaveBeenCalledWith({ where: { - author: { - first_name: Like('%John%'), - last_name: Like('%John%'), - }, + author: { first_name: Like('%John%'), last_name: Like('%John%') }, title: Like('%Test%'), content: Like('%Content%'), tags: Like('%test%'), @@ -195,9 +359,13 @@ describe('BlogService', () => { page_size: 10, }; - const expectedResponse = { - status_code: 404, - message: 'no_results_found_for_the_provided_search_criteria', + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[], 0]); + + const result = await service.searchBlogs(query); + + expect(result).toEqual({ + status_code: HttpStatus.NOT_FOUND, + message: 'No results found.', data: { current_page: 1, total_pages: 0, @@ -210,13 +378,7 @@ describe('BlogService', () => { prev_page: null, }, }, - }; - - jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[], 0]); - - const result = await service.searchBlogs(query); - - expect(result).toEqual(expectedResponse); + }); }); it('should validate empty query values and throw an error', async () => {