From b11caa0e0ee2fc167bcb3c28b0e7460b22067db2 Mon Sep 17 00:00:00 2001 From: Ali Agboola <81039794+sage-ali@users.noreply.github.com> Date: Fri, 28 Feb 2025 08:16:58 +0000 Subject: [PATCH 1/5] feat(blogs): Add date properties to Dto and entity for soft delete and others --- src/modules/blogs/dtos/blog-response.dto.ts | 9 +++++++++ src/modules/blogs/dtos/blog.dto.ts | 2 +- src/modules/blogs/entities/blog.entity.ts | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/modules/blogs/dtos/blog-response.dto.ts b/src/modules/blogs/dtos/blog-response.dto.ts index 04814b643..94a5112c8 100644 --- a/src/modules/blogs/dtos/blog-response.dto.ts +++ b/src/modules/blogs/dtos/blog-response.dto.ts @@ -21,4 +21,13 @@ export class BlogResponseDto { @ApiProperty({ description: 'The creation date of the blog' }) created_at: Date; + + @ApiProperty({ description: 'The published date of the blog' }) + published_date?: Date; + + @ApiProperty({ description: 'The last updated date of the blog' }) + last_updated_date?: Date; + + @ApiProperty({ description: 'The deletion date of the blog', required: false }) + deletedAt?: Date; } diff --git a/src/modules/blogs/dtos/blog.dto.ts b/src/modules/blogs/dtos/blog.dto.ts index 8a7d914af..02950433c 100644 --- a/src/modules/blogs/dtos/blog.dto.ts +++ b/src/modules/blogs/dtos/blog.dto.ts @@ -20,5 +20,5 @@ export class BlogDto { author: string; @ApiProperty({ description: 'The creation date of the blog' }) - published_date: Date; + published_date?: Date; } diff --git a/src/modules/blogs/entities/blog.entity.ts b/src/modules/blogs/entities/blog.entity.ts index 354292ece..856891754 100644 --- a/src/modules/blogs/entities/blog.entity.ts +++ b/src/modules/blogs/entities/blog.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Column, ManyToOne } from 'typeorm'; +import { Entity, Column, ManyToOne, DeleteDateColumn, BeforeUpdate } from 'typeorm'; import { AbstractBaseEntity } from '../../../entities/base.entity'; import { User } from '../../user/entities/user.entity'; @@ -16,6 +16,20 @@ export class Blog extends AbstractBaseEntity { @Column('simple-array', { nullable: true }) image_urls?: string[]; + @DeleteDateColumn({ nullable: true, default: null }) + deletedAt?: Date; + + @Column({ nullable: true, default: null }) + published_date?: Date; + + @Column({ nullable: true, default: null }) + last_updated_date?: Date; + @ManyToOne(() => User, user => user.blogs) author: User; + + @BeforeUpdate() + updateDate() { + this.last_updated_date = new Date(); + } } From 29df0601b877f882a963803ecc6870498d11a5b2 Mon Sep 17 00:00:00 2001 From: Ali Agboola <81039794+sage-ali@users.noreply.github.com> Date: Fri, 28 Feb 2025 21:34:24 +0000 Subject: [PATCH 2/5] feat(blogs): Update the delete to soft delete and fix response type with test --- src/modules/blogs/blogs.service.ts | 120 +++++++++--- src/modules/blogs/tests/blogs.service.spec.ts | 182 +++++++++++++++++- 2 files changed, 267 insertions(+), 35 deletions(-) diff --git a/src/modules/blogs/blogs.service.ts b/src/modules/blogs/blogs.service.ts index c5ee2f6d6..6a14a1a26 100644 --- a/src/modules/blogs/blogs.service.ts +++ b/src/modules/blogs/blogs.service.ts @@ -1,7 +1,7 @@ import * as SYS_MSG from '@shared/constants/SystemMessages'; import { HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, Like, MoreThanOrEqual, FindOptionsWhere } from 'typeorm'; +import { Repository, Like, MoreThanOrEqual, FindOptionsWhere, Not, IsNull } from 'typeorm'; import { Blog } from './entities/blog.entity'; import { CreateBlogDto } from './dtos/create-blog.dto'; import { UpdateBlogDto } from './dtos/update-blog.dto'; @@ -52,21 +52,28 @@ export class BlogService { }; } - async getSingleBlog(blogId: string, user: User): Promise { - const singleBlog = await this.blogRepository.findOneBy({ id: blogId }); + async getSingleBlog( + blogId: string, + user: User + ): Promise<{ + status_code: number; + message: string; + data: BlogResponseDto; + }> { + const singleBlog = await this.blogRepository.findOneBy({ id: blogId, deletedAt: null }); const fullName = await this.fetchUserById(user.id); if (!singleBlog) { throw new CustomHttpException(SYS_MSG.BLOG_NOT_FOUND, HttpStatus.NOT_FOUND); } - const { id, created_at, updated_at, ...rest } = singleBlog; + const { id, created_at, ...rest } = singleBlog; const author = `${fullName.first_name} ${fullName.last_name}`; return { status_code: 200, message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, - data: { blog_id: id, ...rest, author, published_date: created_at }, + data: { blog_id: id, ...rest, author, published_date: created_at, created_at }, }; } @@ -96,46 +103,54 @@ export class BlogService { created_at: updatedBlog.created_at, }; } + async deleteBlogPost(id: string): Promise { const blog = await this.blogRepository.findOne({ where: { id } }); if (!blog) { throw new CustomHttpException('Blog post with this id does not exist.', HttpStatus.NOT_FOUND); - } else await this.blogRepository.remove(blog); + } else await this.blogRepository.softRemove(blog); } async getAllBlogs( page: number, - pageSize: number + pageSize: number, + includeDeleted: boolean = false ): Promise<{ status_code: number; message: string; data: { currentPage: number; totalPages: number; totalResults: number; blogs: BlogResponseDto[]; meta: any }; }> { - const skip = (page - 1) * pageSize; - - const [result, total] = await this.blogRepository.findAndCount({ - skip, - take: pageSize, - relations: ['author'], - }); - - const data = this.mapBlogResults(result); - const totalPages = Math.ceil(total / pageSize); + const result = await this.getBlogs(page, pageSize, includeDeleted ? {} : { deletedAt: null }); + return { + status_code: result.status_code, + message: result.message, + data: { + ...result.data, + blogs: includeDeleted + ? result.data.blogs + : result.data.blogs.map(blog => { + const { deletedAt, ...rest } = blog; + return rest; + }), + }, + }; + } + async getDeletedBlogs( + page: number, + pageSize: number + ): Promise<{ + status_code: number; + message: string; + data: { currentPage: number; totalPages: number; totalResults: number; blogs: BlogResponseDto[]; meta: any }; + }> { + const result = await this.getBlogs(page, pageSize, { deletedAt: Not(IsNull()) }); return { - status_code: HttpStatus.OK, - message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + status_code: result.status_code, + message: result.message, data: { - currentPage: page, - totalPages, - totalResults: total, - blogs: data, - meta: { - hasNext: page < totalPages, - total, - nextPage: page < totalPages ? page + 1 : null, - prevPage: page > 1 ? page - 1 : null, - }, + ...result.data, + blogs: result.data.blogs, }, }; } @@ -188,7 +203,12 @@ export class BlogService { current_page: page, total_pages: totalPages, total_results: total, - blogs: data, + blogs: data + .filter(blog => !blog.deletedAt) + .map(blog => { + const { deletedAt, ...rest } = blog; + return rest; + }), meta: { has_next: page < totalPages, total, @@ -199,6 +219,45 @@ export class BlogService { }; } + private async getBlogs( + page: number, + pageSize: number, + whereCondition: FindOptionsWhere + ): Promise<{ + status_code: number; + message: string; + data: { currentPage: number; totalPages: number; totalResults: number; blogs: BlogResponseDto[]; meta: any }; + }> { + const skip = (page - 1) * pageSize; + + const [result, total] = await this.blogRepository.findAndCount({ + where: whereCondition, + skip, + take: pageSize, + relations: ['author'], + }); + + const data = this.mapBlogResults(result); + const totalPages = Math.ceil(total / pageSize); + + return { + status_code: HttpStatus.OK, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: page, + totalPages, + totalResults: total, + blogs: data, + meta: { + hasNext: page < totalPages, + total, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }, + }, + }; + } + private buildWhereClause(query: any): FindOptionsWhere { const where: FindOptionsWhere = {}; @@ -249,6 +308,7 @@ export class BlogService { image_urls: blog.image_urls, author: author_name, created_at: blog.created_at, + deletedAt: blog.deletedAt, }; }); } diff --git a/src/modules/blogs/tests/blogs.service.spec.ts b/src/modules/blogs/tests/blogs.service.spec.ts index c11bf770a..8c9f452e3 100644 --- a/src/modules/blogs/tests/blogs.service.spec.ts +++ b/src/modules/blogs/tests/blogs.service.spec.ts @@ -3,7 +3,7 @@ import 'reflect-metadata'; import * as SYS_MSG from '@shared/constants/SystemMessages'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository, Like, MoreThanOrEqual } from 'typeorm'; +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'; @@ -24,6 +24,7 @@ describe('BlogService', () => { findOneBy: jest.fn(), findOne: jest.fn(), remove: jest.fn(), + softRemove: jest.fn(), }); beforeEach(async () => { @@ -153,6 +154,7 @@ describe('BlogService', () => { image_urls: ['http://example.com/image.jpg'], author: 'John Doe', created_at: new Date('2023-01-01'), + deletedAt: undefined, }, ], meta: { @@ -243,7 +245,7 @@ describe('BlogService', () => { blog.tags = ['test']; blog.image_urls = ['http://example.com/image.jpg']; blog.created_at = new Date(); - blog.updated_at = new Date(); + blog.deletedAt = null; jest.spyOn(blogRepository, 'findOneBy').mockResolvedValue(blog); jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); @@ -261,9 +263,11 @@ describe('BlogService', () => { image_urls: blog.image_urls, published_date: blog.created_at, author: 'John Doe', + created_at: blog.created_at, + deletedAt: blog.deletedAt, }, }); - expect(blogRepository.findOneBy).toHaveBeenCalledWith({ id: blogId }); + expect(blogRepository.findOneBy).toHaveBeenCalledWith({ id: blogId, deletedAt: null }); expect(userRepository.findOne).toHaveBeenCalledWith({ where: { id: user.id }, select: ['first_name', 'last_name'], @@ -290,12 +294,12 @@ describe('BlogService', () => { blog.id = 'blog-id'; jest.spyOn(blogRepository, 'findOne').mockResolvedValue(blog); - jest.spyOn(blogRepository, 'remove').mockResolvedValue(undefined); + jest.spyOn(blogRepository, 'softRemove').mockResolvedValue(undefined); await service.deleteBlogPost('blog-id'); expect(blogRepository.findOne).toHaveBeenCalledWith({ where: { id: 'blog-id' } }); - expect(blogRepository.remove).toHaveBeenCalledWith(blog); + expect(blogRepository.softRemove).toHaveBeenCalledWith(blog); }); it('should throw a 404 error if blog not found', async () => { @@ -304,4 +308,172 @@ describe('BlogService', () => { await expect(service.deleteBlogPost('blog-id')).rejects.toThrow('Blog post with this id does not exist'); }); }); + + describe('getAllBlogs', () => { + it('should return all blogs excluding deleted', async () => { + 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 = new User(); + blog.author.first_name = 'John'; + blog.author.last_name = 'Doe'; + blog.created_at = new Date(); + blog.updated_at = new Date(); + + const expectedResponse = { + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 1, + totalPages: 1, + totalResults: 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, + deletedAt: undefined, + }, + ], + meta: { + hasNext: false, + total: 1, + nextPage: null, + prevPage: null, + }, + }, + }; + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); + + const result = await service.getAllBlogs(1, 10); + + expect(result).toEqual(expectedResponse); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { deletedAt: null }, + skip: 0, + take: 10, + relations: ['author'], + }); + }); + + it('should return all blogs including deleted', async () => { + 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 = new User(); + blog.author.first_name = 'John'; + blog.author.last_name = 'Doe'; + blog.created_at = new Date(); + blog.updated_at = new Date(); + blog.deletedAt = new Date(); + + const expectedResponse = { + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 1, + totalPages: 1, + totalResults: 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, + deletedAt: blog.deletedAt, + }, + ], + meta: { + hasNext: false, + total: 1, + nextPage: null, + prevPage: null, + }, + }, + }; + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); + + const result = await service.getAllBlogs(1, 10, true); + + expect(result).toEqual(expectedResponse); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: {}, + skip: 0, + take: 10, + relations: ['author'], + }); + }); + }); + + describe('getDeletedBlogs', () => { + it('should return all deleted blogs', async () => { + 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 = new User(); + blog.author.first_name = 'John'; + blog.author.last_name = 'Doe'; + blog.created_at = new Date(); + blog.updated_at = new Date(); + blog.deletedAt = new Date(); + + const expectedResponse = { + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 1, + totalPages: 1, + totalResults: 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, + deletedAt: blog.deletedAt, + }, + ], + meta: { + hasNext: false, + total: 1, + nextPage: null, + prevPage: null, + }, + }, + }; + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); + + const result = await service.getDeletedBlogs(1, 10); + + expect(result).toEqual(expectedResponse); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { deletedAt: Not(IsNull()) }, + skip: 0, + take: 10, + relations: ['author'], + }); + }); + }); }); From 5088a03badd1b0b41d9565820748715330e631f0 Mon Sep 17 00:00:00 2001 From: Ali Agboola <81039794+sage-ali@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:03:18 +0000 Subject: [PATCH 3/5] fix(blogs): fix the `getAllBlogs` method and add robust test cases --- src/modules/blogs/blogs.service.ts | 10 +- src/modules/blogs/tests/blogs.service.spec.ts | 380 ++++++++++++++++++ 2 files changed, 386 insertions(+), 4 deletions(-) diff --git a/src/modules/blogs/blogs.service.ts b/src/modules/blogs/blogs.service.ts index 6a14a1a26..e9ce8ce8f 100644 --- a/src/modules/blogs/blogs.service.ts +++ b/src/modules/blogs/blogs.service.ts @@ -128,10 +128,12 @@ export class BlogService { ...result.data, blogs: includeDeleted ? result.data.blogs - : result.data.blogs.map(blog => { - const { deletedAt, ...rest } = blog; - return rest; - }), + : result.data.blogs + .filter(blog => !blog.deletedAt) + .map(blog => { + const { deletedAt, ...rest } = blog; + return rest; + }), }, }; } diff --git a/src/modules/blogs/tests/blogs.service.spec.ts b/src/modules/blogs/tests/blogs.service.spec.ts index 8c9f452e3..5505c564f 100644 --- a/src/modules/blogs/tests/blogs.service.spec.ts +++ b/src/modules/blogs/tests/blogs.service.spec.ts @@ -418,6 +418,228 @@ describe('BlogService', () => { relations: ['author'], }); }); + + it('should return an empty list if no blogs are found', async () => { + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[], 0]); + + const result = await service.getAllBlogs(1, 10); + + expect(result).toEqual({ + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 1, + totalPages: 0, + totalResults: 0, + blogs: [], + meta: { + hasNext: false, + total: 0, + nextPage: null, + prevPage: null, + }, + }, + }); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { deletedAt: null }, + skip: 0, + take: 10, + relations: ['author'], + }); + }); + + it('should handle pagination correctly', async () => { + 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 = new User(); + blog.author.first_name = 'John'; + blog.author.last_name = 'Doe'; + blog.created_at = new Date(); + blog.updated_at = new Date(); + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); + + const result = await service.getAllBlogs(2, 1); + + expect(result).toEqual({ + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 2, + totalPages: 1, + totalResults: 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, + deletedAt: undefined, + }, + ], + meta: { + hasNext: false, + total: 1, + nextPage: null, + prevPage: 1, + }, + }, + }); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { deletedAt: null }, + skip: 1, + take: 1, + relations: ['author'], + }); + }); + + it('should return all blogs excluding deleted with multiple mock data', async () => { + const blog1 = new Blog(); + blog1.id = 'blog-id-1'; + blog1.title = 'Test Blog 1'; + blog1.content = 'Test Content 1'; + blog1.tags = ['test1']; + blog1.image_urls = ['http://example.com/image1.jpg']; + blog1.author = new User(); + blog1.author.first_name = 'John'; + blog1.author.last_name = 'Doe'; + blog1.created_at = new Date(); + blog1.updated_at = new Date(); + blog1.deletedAt = null; + + const blog2 = new Blog(); + blog2.id = 'blog-id-2'; + blog2.title = 'Test Blog 2'; + blog2.content = 'Test Content 2'; + blog2.tags = ['test2']; + blog2.image_urls = ['http://example.com/image2.jpg']; + blog2.author = new User(); + blog2.author.first_name = 'Jane'; + blog2.author.last_name = 'Doe'; + blog2.created_at = new Date(); + blog2.updated_at = new Date(); + blog2.deletedAt = new Date(); + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog1, blog2], 2]); + + const result = await service.getAllBlogs(1, 10); + + expect(result).toEqual({ + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 1, + totalPages: 1, + totalResults: 2, + blogs: [ + { + blog_id: 'blog-id-1', + title: 'Test Blog 1', + content: 'Test Content 1', + tags: ['test1'], + image_urls: ['http://example.com/image1.jpg'], + author: 'John Doe', + created_at: blog1.created_at, + }, + ], + meta: { + hasNext: false, + total: 2, + nextPage: null, + prevPage: null, + }, + }, + }); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { deletedAt: null }, + skip: 0, + take: 10, + relations: ['author'], + }); + }); + + it('should return all blogs including deleted with multiple mock data', async () => { + const blog1 = new Blog(); + blog1.id = 'blog-id-1'; + blog1.title = 'Test Blog 1'; + blog1.content = 'Test Content 1'; + blog1.tags = ['test1']; + blog1.image_urls = ['http://example.com/image1.jpg']; + blog1.author = new User(); + blog1.author.first_name = 'John'; + blog1.author.last_name = 'Doe'; + blog1.created_at = new Date(); + blog1.updated_at = new Date(); + blog1.deletedAt = null; + + const blog2 = new Blog(); + blog2.id = 'blog-id-2'; + blog2.title = 'Test Blog 2'; + blog2.content = 'Test Content 2'; + blog2.tags = ['test2']; + blog2.image_urls = ['http://example.com/image2.jpg']; + blog2.author = new User(); + blog2.author.first_name = 'Jane'; + blog2.author.last_name = 'Doe'; + blog2.created_at = new Date(); + blog2.updated_at = new Date(); + blog2.deletedAt = new Date(); + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog1, blog2], 2]); + + const result = await service.getAllBlogs(1, 10, true); + + expect(result).toEqual({ + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 1, + totalPages: 1, + totalResults: 2, + blogs: [ + { + blog_id: 'blog-id-1', + title: 'Test Blog 1', + content: 'Test Content 1', + tags: ['test1'], + image_urls: ['http://example.com/image1.jpg'], + author: 'John Doe', + created_at: blog1.created_at, + deletedAt: null, + }, + { + blog_id: 'blog-id-2', + title: 'Test Blog 2', + content: 'Test Content 2', + tags: ['test2'], + image_urls: ['http://example.com/image2.jpg'], + author: 'Jane Doe', + created_at: blog2.created_at, + deletedAt: blog2.deletedAt, + }, + ], + meta: { + hasNext: false, + total: 2, + nextPage: null, + prevPage: null, + }, + }, + }); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: {}, + skip: 0, + take: 10, + relations: ['author'], + }); + }); }); describe('getDeletedBlogs', () => { @@ -475,5 +697,163 @@ describe('BlogService', () => { relations: ['author'], }); }); + + it('should return an empty list if no deleted blogs are found', async () => { + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[], 0]); + + const result = await service.getDeletedBlogs(1, 10); + + expect(result).toEqual({ + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 1, + totalPages: 0, + totalResults: 0, + blogs: [], + meta: { + hasNext: false, + total: 0, + nextPage: null, + prevPage: null, + }, + }, + }); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { deletedAt: Not(IsNull()) }, + skip: 0, + take: 10, + relations: ['author'], + }); + }); + + it('should handle pagination correctly for deleted blogs', async () => { + 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 = new User(); + blog.author.first_name = 'John'; + blog.author.last_name = 'Doe'; + blog.created_at = new Date(); + blog.updated_at = new Date(); + blog.deletedAt = new Date(); + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog], 1]); + + const result = await service.getDeletedBlogs(2, 1); + + expect(result).toEqual({ + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 2, + totalPages: 1, + totalResults: 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, + deletedAt: blog.deletedAt, + }, + ], + meta: { + hasNext: false, + total: 1, + nextPage: null, + prevPage: 1, + }, + }, + }); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { deletedAt: Not(IsNull()) }, + skip: 1, + take: 1, + relations: ['author'], + }); + }); + + it('should return all deleted blogs with multiple mock data', async () => { + const blog1 = new Blog(); + blog1.id = 'blog-id-1'; + blog1.title = 'Test Blog 1'; + blog1.content = 'Test Content 1'; + blog1.tags = ['test1']; + blog1.image_urls = ['http://example.com/image1.jpg']; + blog1.author = new User(); + blog1.author.first_name = 'John'; + blog1.author.last_name = 'Doe'; + blog1.created_at = new Date(); + blog1.updated_at = new Date(); + blog1.deletedAt = new Date(); + + const blog2 = new Blog(); + blog2.id = 'blog-id-2'; + blog2.title = 'Test Blog 2'; + blog2.content = 'Test Content 2'; + blog2.tags = ['test2']; + blog2.image_urls = ['http://example.com/image2.jpg']; + blog2.author = new User(); + blog2.author.first_name = 'Jane'; + blog2.author.last_name = 'Doe'; + blog2.created_at = new Date(); + blog2.updated_at = new Date(); + blog2.deletedAt = new Date(); + + jest.spyOn(blogRepository, 'findAndCount').mockResolvedValue([[blog1, blog2], 2]); + + const result = await service.getDeletedBlogs(1, 10); + + expect(result).toEqual({ + status_code: 200, + message: SYS_MSG.BLOG_FETCHED_SUCCESSFUL, + data: { + currentPage: 1, + totalPages: 1, + totalResults: 2, + blogs: [ + { + blog_id: 'blog-id-1', + title: 'Test Blog 1', + content: 'Test Content 1', + tags: ['test1'], + image_urls: ['http://example.com/image1.jpg'], + author: 'John Doe', + created_at: blog1.created_at, + deletedAt: blog1.deletedAt, + }, + { + blog_id: 'blog-id-2', + title: 'Test Blog 2', + content: 'Test Content 2', + tags: ['test2'], + image_urls: ['http://example.com/image2.jpg'], + author: 'Jane Doe', + created_at: blog2.created_at, + deletedAt: blog2.deletedAt, + }, + ], + meta: { + hasNext: false, + total: 2, + nextPage: null, + prevPage: null, + }, + }, + }); + expect(blogRepository.findAndCount).toHaveBeenCalledWith({ + where: { deletedAt: Not(IsNull()) }, + skip: 0, + take: 10, + relations: ['author'], + }); + }); }); }); From c4983f00f5445f0149c4659462bd4bdf214e97f7 Mon Sep 17 00:00:00 2001 From: Ali Agboola <81039794+sage-ali@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:04:35 +0000 Subject: [PATCH 4/5] fix(blogs): Fix GET blog by id response type --- src/modules/blogs/blogs.controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/blogs/blogs.controller.ts b/src/modules/blogs/blogs.controller.ts index db0a8a36d..f7f8ebc0f 100644 --- a/src/modules/blogs/blogs.controller.ts +++ b/src/modules/blogs/blogs.controller.ts @@ -80,7 +80,14 @@ export class BlogController { @ApiResponse({ status: 200, description: 'Blog fetched successfully.', type: BlogDto }) @ApiResponse({ status: 404, description: 'Blog not found.' }) @ApiResponse({ status: 500, description: 'Internal server error.' }) - async getSingleBlog(@Param('id', new ParseUUIDPipe()) id: string, @Request() req): Promise { + async getSingleBlog( + @Param('id', new ParseUUIDPipe()) id: string, + @Request() req + ): Promise<{ + status_code: number; + message: string; + data: BlogDto; + }> { return await this.blogService.getSingleBlog(id, req.user); } From abfd92ac80d90a03c8fae7be94d6db61fd8a6b60 Mon Sep 17 00:00:00 2001 From: Ali Agboola <81039794+sage-ali@users.noreply.github.com> Date: Sun, 2 Mar 2025 18:45:09 +0000 Subject: [PATCH 5/5] fix(blogs): Revert deletion of `updated_at` property and unnecessary additions --- src/modules/blogs/blogs.service.ts | 2 +- src/modules/blogs/dtos/blog-response.dto.ts | 3 --- src/modules/blogs/entities/blog.entity.ts | 8 -------- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/modules/blogs/blogs.service.ts b/src/modules/blogs/blogs.service.ts index e9ce8ce8f..e7c8e33c9 100644 --- a/src/modules/blogs/blogs.service.ts +++ b/src/modules/blogs/blogs.service.ts @@ -67,7 +67,7 @@ export class BlogService { throw new CustomHttpException(SYS_MSG.BLOG_NOT_FOUND, HttpStatus.NOT_FOUND); } - const { id, created_at, ...rest } = singleBlog; + const { id, created_at, updated_at, ...rest } = singleBlog; const author = `${fullName.first_name} ${fullName.last_name}`; return { diff --git a/src/modules/blogs/dtos/blog-response.dto.ts b/src/modules/blogs/dtos/blog-response.dto.ts index 94a5112c8..4e17f23a0 100644 --- a/src/modules/blogs/dtos/blog-response.dto.ts +++ b/src/modules/blogs/dtos/blog-response.dto.ts @@ -25,9 +25,6 @@ export class BlogResponseDto { @ApiProperty({ description: 'The published date of the blog' }) published_date?: Date; - @ApiProperty({ description: 'The last updated date of the blog' }) - last_updated_date?: Date; - @ApiProperty({ description: 'The deletion date of the blog', required: false }) deletedAt?: Date; } diff --git a/src/modules/blogs/entities/blog.entity.ts b/src/modules/blogs/entities/blog.entity.ts index 856891754..7afee40b5 100644 --- a/src/modules/blogs/entities/blog.entity.ts +++ b/src/modules/blogs/entities/blog.entity.ts @@ -22,14 +22,6 @@ export class Blog extends AbstractBaseEntity { @Column({ nullable: true, default: null }) published_date?: Date; - @Column({ nullable: true, default: null }) - last_updated_date?: Date; - @ManyToOne(() => User, user => user.blogs) author: User; - - @BeforeUpdate() - updateDate() { - this.last_updated_date = new Date(); - } }