chore: add blog blog comment and blog category crud
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
import { ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { SKIP_AUTH_KEY } from "../../../common/decorators/skip-auth.decorator";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const skipAuth = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH_KEY, [context.getHandler(), context.getClass()]);
|
||||
|
||||
if (skipAuth) return true;
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ArrayMinSize, IsArray, IsBoolean, IsNotEmpty, IsString, IsUUID, MaxLength } from "class-validator";
|
||||
|
||||
import { BlogMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CreateBlogDto {
|
||||
@IsNotEmpty({ message: BlogMessage.TITLE_REQUIRED })
|
||||
@IsString({ message: BlogMessage.TITLE_STRING })
|
||||
@MaxLength(150, { message: BlogMessage.TITLE_MAX_LENGTH })
|
||||
@ApiProperty({ description: "Title of the blog", example: "Blog Title" })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.PREVIEW_CONTENT_REQUIRED })
|
||||
@IsString({ message: BlogMessage.PREVIEW_CONTENT_STRING })
|
||||
@MaxLength(150, { message: BlogMessage.PREVIEW_CONTENT_MAX_LENGTH })
|
||||
@ApiProperty({ description: "Preview content of the blog", example: "Preview content of the blog" })
|
||||
previewContent: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.CONTENT_REQUIRED })
|
||||
@IsString({ message: BlogMessage.CONTENT_STRING })
|
||||
@MaxLength(600, { message: BlogMessage.CONTENT_MAX_LENGTH })
|
||||
@ApiProperty({ description: "Content of the blog", example: "Content of the blog" })
|
||||
content: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.IMAGE_URL_REQUIRED })
|
||||
@IsString({ message: BlogMessage.IMAGE_URL_STRING })
|
||||
@MaxLength(150, { message: BlogMessage.IMAGE_URL_MAX_LENGTH })
|
||||
imageUrl: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.META_TITLE_REQUIRED })
|
||||
@IsString({ message: BlogMessage.META_TITLE_STRING })
|
||||
@MaxLength(60, { message: BlogMessage.META_TITLE_MAX_LENGTH })
|
||||
@ApiProperty({ description: "Meta title of the blog", example: "Meta title of the blog" })
|
||||
metaTitle: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.META_DESCRIPTION_REQUIRED })
|
||||
@IsString({ message: BlogMessage.META_DESCRIPTION_STRING })
|
||||
@MaxLength(160, { message: BlogMessage.META_DESCRIPTION_MAX_LENGTH })
|
||||
@ApiProperty({ description: "Meta description of the blog", example: "Meta description of the blog" })
|
||||
metaDescription: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.TAGS_REQUIRED })
|
||||
@IsArray({ message: BlogMessage.TAGS_ARRAY })
|
||||
@IsString({ each: true, message: BlogMessage.TAGS_STRING })
|
||||
@ArrayMinSize(1, { message: BlogMessage.TAGS_MIN_SIZE })
|
||||
@ApiProperty({ description: "Tags of the blog", example: ["tag1", "tag2", "tag3"] })
|
||||
tags: string[];
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.CATEGORY_ID_REQUIRED })
|
||||
@IsUUID("4", { message: BlogMessage.CATEGORY_ID_UUID })
|
||||
@ApiProperty({ description: "Category ID of the blog", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
categoryId: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.IS_ACTIVE_REQUIRED })
|
||||
@IsBoolean({ message: BlogMessage.IS_ACTIVE_SHOULD_BE_A_BOOLEAN })
|
||||
@ApiProperty({ description: "Is active of the blog", example: true })
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsNotEmpty, IsString, IsUrl, MaxLength } from "class-validator";
|
||||
|
||||
import { BlogMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CreateCategoryDto {
|
||||
@IsNotEmpty({ message: BlogMessage.TITLE_REQUIRED })
|
||||
@IsString({ message: BlogMessage.TITLE_STRING })
|
||||
@MaxLength(100, { message: BlogMessage.TITLE_MAX_LENGTH })
|
||||
@ApiProperty({ description: "The title of the category", example: "This is the title of the category" })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.DESCRIPTION_REQUIRED })
|
||||
@IsString({ message: BlogMessage.DESCRIPTION_STRING })
|
||||
@MaxLength(255, { message: BlogMessage.DESCRIPTION_MAX_LENGTH })
|
||||
@ApiProperty({ description: "The description of the category", example: "This is the description of the category" })
|
||||
description: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.ICON_URL_REQUIRED })
|
||||
@IsUrl({ protocols: ["http", "https"] }, { message: BlogMessage.ICON_URL_INVALID })
|
||||
@MaxLength(255, { message: BlogMessage.ICON_URL_MAX_LENGTH })
|
||||
@ApiProperty({ description: "The URL of the icon for the category", example: "https://example.com/icon.png" })
|
||||
iconUrl: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.IS_ACTIVE_REQUIRED })
|
||||
@IsBoolean({ message: BlogMessage.IS_ACTIVE_SHOULD_BE_A_BOOLEAN })
|
||||
@ApiProperty({ description: "The status of the category", example: true })
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
import { BlogMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CreateCommentDto {
|
||||
@IsNotEmpty({ message: BlogMessage.TITLE_REQUIRED })
|
||||
@IsString({ message: BlogMessage.TITLE_STRING })
|
||||
@ApiProperty({ description: "the title of comment" })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: BlogMessage.COMMENT_CONTENT_REQUIRED })
|
||||
@IsString({ message: BlogMessage.COMMENT_CONTENT_STRING })
|
||||
@ApiProperty({ description: "the content of comment", example: "sd" })
|
||||
content: string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
import { BlogMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class BlogSlugDto {
|
||||
@IsNotEmpty({ message: BlogMessage.SLUG_NOT_EMPTY })
|
||||
@IsString({ message: BlogMessage.SLUG_STRING })
|
||||
@ApiProperty({ description: "The slug of the blog", example: "blog-slug" })
|
||||
slug: string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ApiPropertyOptional, PickType } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsDateString, IsIn, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
import { BlogMessage, CategoryMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class BlogListSearchQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@IsString({ message: BlogMessage.SEARCH_QUERY_STRING })
|
||||
@ApiPropertyOptional({ description: "Search query", example: "search query" })
|
||||
q?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID(4, { message: CategoryMessage.CAT_ID_SHOULD_BE_UUID })
|
||||
@ApiPropertyOptional({ description: "Category id", example: "8b1e8b1e-8b1e-8b1e-8b1e-8b1e8b1e8b1e" })
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString({}, { message: BlogMessage.INVALID_DATE })
|
||||
@ApiPropertyOptional({ description: "since query", example: "2025-01-27" })
|
||||
since?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsIn([1, 0], { message: BlogMessage.IS_ACTIVE_SHOULD_BE_1_0 })
|
||||
@ApiPropertyOptional({ description: "blog status", example: 1 })
|
||||
isActive?: number;
|
||||
}
|
||||
|
||||
export class BlogSearchQueryDto extends PickType(BlogListSearchQueryDto, ["limit", "page", "categoryId"] as const) {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateBlogDto } from "./create-blog.dto";
|
||||
|
||||
export class UpdateBlogDto extends PartialType(CreateBlogDto) {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateCategoryDto } from "./create-category.dto";
|
||||
|
||||
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum, IsNotEmpty } from "class-validator";
|
||||
|
||||
import { BlogMessage } from "../../../common/enums/message.enum";
|
||||
import { CommentStatus } from "../enums/comment-status.enum";
|
||||
|
||||
export class UpdateCommentStatusDto {
|
||||
@IsNotEmpty({ message: BlogMessage.COMMENT_STATUS_REQUIRED })
|
||||
@IsEnum(CommentStatus)
|
||||
@ApiProperty({ description: "the status of comment", example: CommentStatus.APPROVED, enum: CommentStatus })
|
||||
status: CommentStatus;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { BlogsController } from "./controllers/blogs.controller";
|
||||
import { BlogCategory } from "./entities/blog-category.entity";
|
||||
import { BlogComment } from "./entities/blog-comment.entity";
|
||||
import { Blog } from "./entities/blog.entity";
|
||||
import { BlogsService } from "./providers/blogs.service";
|
||||
import { BlogCategoriesRepository } from "./repositories/blog-categories.repository";
|
||||
import { BlogCommentsRepository } from "./repositories/blog-comments.repository";
|
||||
import { BlogsRepository } from "./repositories/blogs.repository";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Blog, BlogComment, BlogCategory])],
|
||||
controllers: [BlogsController],
|
||||
providers: [BlogsService, BlogsRepository, BlogCommentsRepository, BlogCategoriesRepository],
|
||||
exports: [BlogsService],
|
||||
})
|
||||
export class BlogsModule {}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { AdminRoute } from "../../../common/decorators/admin.decorator";
|
||||
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
|
||||
import { Pagination } from "../../../common/decorators/pagination.decorator";
|
||||
import { PermissionsDec } from "../../../common/decorators/permission.decorator";
|
||||
import { SkipAuth } from "../../../common/decorators/skip-auth.decorator";
|
||||
import { UserDec } from "../../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||
import { CategoryListSearchQueryDto } from "../../danak-services/DTO/category-search-query.dto";
|
||||
import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
import { CreateBlogDto } from "../DTO/create-blog.dto";
|
||||
import { CreateCategoryDto } from "../DTO/create-category.dto";
|
||||
import { CreateCommentDto } from "../DTO/create-comment.dto";
|
||||
import { BlogSlugDto } from "../DTO/get-blog-by-slug.dto";
|
||||
import { BlogListSearchQueryDto, BlogSearchQueryDto } from "../DTO/search-blog-query.dto";
|
||||
import { UpdateBlogDto } from "../DTO/update-blog.dto";
|
||||
import { UpdateCategoryDto } from "../DTO/update-category.dto";
|
||||
import { UpdateCommentStatusDto } from "../DTO/update-comment-status.dto";
|
||||
import { BlogsService } from "../providers/blogs.service";
|
||||
|
||||
@Controller("blogs")
|
||||
@AuthGuards()
|
||||
export class BlogsController {
|
||||
constructor(private readonly blogsService: BlogsService) {}
|
||||
|
||||
@ApiOperation({ summary: "Get all public and active blogs with category id" })
|
||||
@SkipAuth()
|
||||
@Get()
|
||||
getBlogsPublic(@Query() queryDto: BlogSearchQueryDto) {
|
||||
return this.blogsService.getBlogsPublic(queryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all blogs (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
@Get("list")
|
||||
getBlogList(@Query() queryDto: BlogListSearchQueryDto) {
|
||||
return this.blogsService.getBlogList(queryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get a blog by id" })
|
||||
@SkipAuth()
|
||||
@Get(":id")
|
||||
getBlog(@Param() paramDto: ParamDto) {
|
||||
return this.blogsService.getBlogById(paramDto.id);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get a blog by slug" })
|
||||
@SkipAuth()
|
||||
@Get("slug/:slug")
|
||||
getBlogBySlug(@Param() paramDto: BlogSlugDto) {
|
||||
return this.blogsService.getBlogBySlug(paramDto.slug);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Create a new blog (admin route)" })
|
||||
@Post()
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
createBlog(@Body() createBlogDto: CreateBlogDto, @UserDec("id") userId: string) {
|
||||
return this.blogsService.createBlog(createBlogDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Update a blog (admin route)" })
|
||||
@Patch(":id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
updateBlog(@Param() paramDto: ParamDto, @Body() updateBlogDto: UpdateBlogDto) {
|
||||
return this.blogsService.updateBlog(paramDto.id, updateBlogDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Delete a blog (admin route) " })
|
||||
@Delete(":id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
deleteBlog(@Param() paramDto: ParamDto) {
|
||||
return this.blogsService.deleteBlog(paramDto.id);
|
||||
}
|
||||
|
||||
//-------------- comments --------------
|
||||
|
||||
@ApiOperation({ summary: "Create a new comment" })
|
||||
@Post(":id/comments")
|
||||
createComment(@Param() paramDto: ParamDto, @Body() createCommentDto: CreateCommentDto, @UserDec("id") userId: string) {
|
||||
return this.blogsService.createComment(paramDto.id, createCommentDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all comments for a blog" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
@Get(":id/comments")
|
||||
getBlogComments(@Param() paramDto: ParamDto) {
|
||||
return this.blogsService.getBlogComments(paramDto.id);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Update the status of a comment (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
@Put("comments/:id/status")
|
||||
updateCommentStatus(@Param() paramDto: ParamDto, @Body() updateCommentStatusDto: UpdateCommentStatusDto) {
|
||||
return this.blogsService.updateCommentStatus(paramDto.id, updateCommentStatusDto);
|
||||
}
|
||||
|
||||
//-------------- categories --------------
|
||||
|
||||
@ApiOperation({ summary: "Get all active categories (admin route)" })
|
||||
@Pagination()
|
||||
@Get("categories/list")
|
||||
getAllActiveCategories(@Query() queryDto: CategoryListSearchQueryDto) {
|
||||
return this.blogsService.getCategories(queryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all active categories (user side)" })
|
||||
@Get("categories/public")
|
||||
getCategoriesUserSide() {
|
||||
return this.blogsService.getCategoriesUserSide();
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get a category by id" })
|
||||
@Get("categories/:id")
|
||||
getCategory(@Param() paramDto: ParamDto) {
|
||||
return this.blogsService.getCategory(paramDto.id);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Create a new category (admin route)" })
|
||||
@Post("categories")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
createCategory(@Body() createCategoryDto: CreateCategoryDto) {
|
||||
return this.blogsService.createCategory(createCategoryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Update a category (admin route)" })
|
||||
@Put("categories/:id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateCategoryDto) {
|
||||
return this.blogsService.updateCategory(paramDto.id, updateCategoryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Delete a category (admin route)" })
|
||||
@Delete("categories/:id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
deleteCategory(@Param() paramDto: ParamDto) {
|
||||
return this.blogsService.deleteCategory(paramDto.id);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "toggle status of categories (admin route)" })
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("categories/toggle-status/:id")
|
||||
toggleCategoryStatus(@Param() paramDto: ParamDto) {
|
||||
return this.blogsService.toggleCategoryStatus(paramDto.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm";
|
||||
|
||||
import { Blog } from "./blog.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
|
||||
@Entity()
|
||||
export class BlogCategory extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 100, unique: true })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255, nullable: true })
|
||||
iconUrl: string;
|
||||
|
||||
@Column({ type: "boolean", default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@OneToMany(() => Blog, (blog) => blog.category)
|
||||
blogs: Blog[];
|
||||
|
||||
@DeleteDateColumn({ type: "timestamptz" })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Column, Entity, ManyToOne } from "typeorm";
|
||||
|
||||
import { Blog } from "./blog.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { CommentStatus } from "../enums/comment-status.enum";
|
||||
|
||||
@Entity()
|
||||
export class BlogComment extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150, nullable: false })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "text" })
|
||||
content: string;
|
||||
|
||||
@Column({ type: "enum", enum: CommentStatus, default: CommentStatus.PENDING })
|
||||
status: CommentStatus;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.blogComments)
|
||||
user: User;
|
||||
|
||||
@ManyToOne(() => Blog, (blog) => blog.comments)
|
||||
blog: Blog;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Column, DeleteDateColumn, Entity, ManyToOne, OneToMany } from "typeorm";
|
||||
|
||||
import { BlogCategory } from "./blog-category.entity";
|
||||
import { BlogComment } from "./blog-comment.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity()
|
||||
export class Blog extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150, unique: true })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "varchar", length: 150, unique: true })
|
||||
slug: string;
|
||||
|
||||
@Column({ type: "text" })
|
||||
previewContent: string;
|
||||
|
||||
@Column({ type: "text" })
|
||||
content: string;
|
||||
|
||||
@Column({ type: "varchar", length: 150, nullable: true })
|
||||
imageUrl: string;
|
||||
|
||||
@Column({ type: "varchar", length: 100, nullable: true })
|
||||
metaTitle: string;
|
||||
|
||||
@Column({ type: "varchar", length: 160, nullable: true })
|
||||
metaDescription: string;
|
||||
|
||||
@Column({ type: "simple-array", nullable: true })
|
||||
tags: string[];
|
||||
|
||||
@Column({ type: "boolean", default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.blogs)
|
||||
author: User;
|
||||
|
||||
@ManyToOne(() => BlogCategory, (category) => category.blogs)
|
||||
category: BlogCategory;
|
||||
|
||||
@OneToMany(() => BlogComment, (comment) => comment.blog)
|
||||
comments: BlogComment[];
|
||||
|
||||
@DeleteDateColumn({ type: "timestamptz" })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum CommentStatus {
|
||||
PENDING = "PENDING",
|
||||
APPROVED = "APPROVED",
|
||||
REJECTED = "REJECTED",
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import slugify from "slugify";
|
||||
import { Not } from "typeorm";
|
||||
|
||||
import { BlogMessage, CommonMessage } from "../../../common/enums/message.enum";
|
||||
import { CategoryListSearchQueryDto } from "../../danak-services/DTO/category-search-query.dto";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { CreateBlogDto } from "../DTO/create-blog.dto";
|
||||
import { CreateCategoryDto } from "../DTO/create-category.dto";
|
||||
import { CreateCommentDto } from "../DTO/create-comment.dto";
|
||||
import { BlogListSearchQueryDto, BlogSearchQueryDto } from "../DTO/search-blog-query.dto";
|
||||
import { UpdateBlogDto } from "../DTO/update-blog.dto";
|
||||
import { UpdateCategoryDto } from "../DTO/update-category.dto";
|
||||
import { UpdateCommentStatusDto } from "../DTO/update-comment-status.dto";
|
||||
import { CommentStatus } from "../enums/comment-status.enum";
|
||||
import { BlogCategoriesRepository } from "../repositories/blog-categories.repository";
|
||||
import { BlogCommentsRepository } from "../repositories/blog-comments.repository";
|
||||
import { BlogsRepository } from "../repositories/blogs.repository";
|
||||
@Injectable()
|
||||
export class BlogsService {
|
||||
constructor(
|
||||
private readonly blogsRepository: BlogsRepository,
|
||||
private readonly blogCommentsRepository: BlogCommentsRepository,
|
||||
private readonly blogCategoriesRepository: BlogCategoriesRepository,
|
||||
private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
//*********************************** */
|
||||
async createCategory(createCategoryDto: CreateCategoryDto) {
|
||||
const existingCategory = await this.blogCategoriesRepository.findOneBy({ title: createCategoryDto.title });
|
||||
if (existingCategory) throw new BadRequestException(BlogMessage.CATEGORY_ALREADY_EXISTS);
|
||||
|
||||
const category = await this.blogCategoriesRepository.createCategory(createCategoryDto);
|
||||
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
category,
|
||||
};
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async updateCategory(id: string, updateCategoryDto: UpdateCategoryDto) {
|
||||
const category = await this.findCategoryById(id);
|
||||
|
||||
if (updateCategoryDto.title) {
|
||||
await this.checkCategoryExists(updateCategoryDto.title, id);
|
||||
}
|
||||
|
||||
await this.blogCategoriesRepository.save({ ...category, ...updateCategoryDto });
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
category,
|
||||
};
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async deleteCategory(id: string) {
|
||||
await this.findCategoryById(id);
|
||||
|
||||
await this.blogCategoriesRepository.softDelete(id);
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async toggleCategoryStatus(id: string) {
|
||||
const category = await this.findCategoryById(id);
|
||||
|
||||
//
|
||||
category.isActive = !category.isActive;
|
||||
await this.blogCategoriesRepository.save(category);
|
||||
|
||||
//
|
||||
return { message: CommonMessage.UPDATE_SUCCESS, category };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getCategory(id: string) {
|
||||
const category = await this.findCategoryById(id);
|
||||
return { category };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getCategoriesUserSide() {
|
||||
const categories = await this.blogCategoriesRepository.getCategoriesUserSide();
|
||||
return { categories };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getCategories(queryDto: CategoryListSearchQueryDto) {
|
||||
const [categories, count] = await this.blogCategoriesRepository.getCategories(queryDto);
|
||||
return { categories, count, paginate: true };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async createBlog(createBlogDto: CreateBlogDto, authorId: string) {
|
||||
const existingBlog = await this.blogsRepository.findOne({ where: { title: createBlogDto.title } });
|
||||
if (existingBlog) throw new BadRequestException(BlogMessage.BLOG_ALREADY_EXISTS);
|
||||
|
||||
const category = await this.findCategoryById(createBlogDto.categoryId);
|
||||
|
||||
const slug = this.generateSlug(createBlogDto.title);
|
||||
|
||||
const blog = this.blogsRepository.create({
|
||||
...createBlogDto,
|
||||
slug,
|
||||
author: { id: authorId },
|
||||
category,
|
||||
});
|
||||
|
||||
await this.blogsRepository.save(blog);
|
||||
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
blog,
|
||||
};
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
|
||||
async updateBlog(id: string, updateBlogDto: UpdateBlogDto) {
|
||||
const blog = await this.findBlogById(id);
|
||||
|
||||
if (updateBlogDto.title) {
|
||||
const existBlog = await this.blogsRepository.findOne({ where: { title: updateBlogDto.title, id: Not(id) } });
|
||||
if (existBlog) throw new BadRequestException(BlogMessage.BLOG_ALREADY_EXISTS);
|
||||
blog.slug = this.generateSlug(updateBlogDto.title);
|
||||
}
|
||||
|
||||
if (updateBlogDto.categoryId) {
|
||||
const category = await this.findCategoryById(updateBlogDto.categoryId);
|
||||
blog.category = category;
|
||||
}
|
||||
|
||||
await this.blogsRepository.save({ ...blog, ...updateBlogDto });
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
blog,
|
||||
};
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async deleteBlog(id: string) {
|
||||
await this.findBlogById(id);
|
||||
|
||||
await this.blogsRepository.softDelete(id);
|
||||
return { message: CommonMessage.DELETED };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getBlogById(id: string) {
|
||||
const blog = await this.findBlogById(id);
|
||||
return { blog };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getBlogBySlug(slug: string) {
|
||||
const blog = await this.blogsRepository.findOneBySlug(slug);
|
||||
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
|
||||
return { blog };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getBlogsPublic(queryDto: BlogSearchQueryDto) {
|
||||
const blogs = await this.blogsRepository.getBlogsForUserSide(queryDto);
|
||||
return { blogs };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getBlogList(queryDto: BlogListSearchQueryDto) {
|
||||
const [blogs, count] = await this.blogsRepository.getBlogListForAdmin(queryDto);
|
||||
return { blogs, count, paginate: true };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async createComment(blogId: string, createCommentDto: CreateCommentDto, userId: string) {
|
||||
const blog = await this.findBlogById(blogId);
|
||||
|
||||
const { user } = await this.usersService.findOneById(userId);
|
||||
|
||||
const comment = this.blogCommentsRepository.create({
|
||||
...createCommentDto,
|
||||
blog,
|
||||
user,
|
||||
status: CommentStatus.PENDING,
|
||||
});
|
||||
|
||||
await this.blogCommentsRepository.save(comment);
|
||||
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
comment,
|
||||
};
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async updateCommentStatus(commentId: string, updateCommentStatusDto: UpdateCommentStatusDto) {
|
||||
const comment = await this.blogCommentsRepository.findOneById(commentId);
|
||||
if (!comment) throw new BadRequestException(BlogMessage.COMMENT_NOT_FOUND);
|
||||
|
||||
comment.status = updateCommentStatusDto.status;
|
||||
await this.blogCommentsRepository.save(comment);
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
comment,
|
||||
};
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getBlogComments(blogId: string) {
|
||||
const comments = await this.blogCommentsRepository.findAllByBlogId(blogId);
|
||||
return { comments };
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
private generateSlug(title: string): string {
|
||||
return slugify(title, { lower: true, strict: true });
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
private async checkCategoryExists(title: string, id?: string) {
|
||||
const existingCategory = await this.blogCategoriesRepository.findOneBy({ title, ...(id && { id: Not(id) }) });
|
||||
if (existingCategory) throw new BadRequestException(BlogMessage.CATEGORY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
private async findCategoryById(id: string) {
|
||||
const category = await this.blogCategoriesRepository.findOneById(id);
|
||||
if (!category) throw new BadRequestException(BlogMessage.CATEGORY_NOT_FOUND);
|
||||
return category;
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
private async findBlogById(id: string) {
|
||||
const blog = await this.blogsRepository.findOneById(id);
|
||||
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
|
||||
return blog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { IsNull, Repository } from "typeorm";
|
||||
|
||||
import { CategoryListSearchQueryDto } from "../../danak-services/DTO/category-search-query.dto";
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { CreateCategoryDto } from "../DTO/create-category.dto";
|
||||
import { BlogCategory } from "../entities/blog-category.entity";
|
||||
|
||||
@Injectable()
|
||||
export class BlogCategoriesRepository extends Repository<BlogCategory> {
|
||||
constructor(@InjectRepository(BlogCategory) repository: Repository<BlogCategory>) {
|
||||
super(repository.target, repository.manager, repository.queryRunner);
|
||||
}
|
||||
|
||||
async findOneById(id: string): Promise<BlogCategory | null> {
|
||||
return this.findOne({
|
||||
where: { id, deletedAt: IsNull() },
|
||||
});
|
||||
}
|
||||
|
||||
async getCategoriesUserSide() {
|
||||
return this.find({
|
||||
where: { isActive: true, deletedAt: IsNull() },
|
||||
order: { createdAt: "DESC" },
|
||||
});
|
||||
}
|
||||
|
||||
async getCategories(queryDto: CategoryListSearchQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const query = this.createQueryBuilder("blogCategory").where("blogCategory.deletedAt IS NULL");
|
||||
|
||||
if (queryDto.q) {
|
||||
query.andWhere("blogCategory.title ILIKE :q OR blogCategory.slug ILIKE :q", { q: `%${queryDto.q}%` });
|
||||
}
|
||||
|
||||
if (queryDto.isActive) {
|
||||
query.andWhere("blogCategory.isActive = :isActive", { isActive: queryDto.isActive });
|
||||
}
|
||||
|
||||
query.skip(skip).take(limit).orderBy({ createdAt: "DESC" });
|
||||
|
||||
return query.getManyAndCount();
|
||||
}
|
||||
|
||||
async createCategory(createBlogCategoryDto: CreateCategoryDto) {
|
||||
const { title, description, iconUrl, isActive } = createBlogCategoryDto;
|
||||
|
||||
const blogCategory = this.create({ title, description, iconUrl, isActive });
|
||||
|
||||
return this.save(blogCategory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { BlogComment } from "../entities/blog-comment.entity";
|
||||
import { CommentStatus } from "../enums/comment-status.enum";
|
||||
|
||||
@Injectable()
|
||||
export class BlogCommentsRepository extends Repository<BlogComment> {
|
||||
constructor(@InjectRepository(BlogComment) blogCommentsRepository: Repository<BlogComment>) {
|
||||
super(blogCommentsRepository.target, blogCommentsRepository.manager, blogCommentsRepository.queryRunner);
|
||||
}
|
||||
|
||||
async findOneById(id: string) {
|
||||
return this.findOne({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async findAllByBlogId(blogId: string) {
|
||||
return this.find({
|
||||
where: { blog: { id: blogId }, status: CommentStatus.APPROVED },
|
||||
order: { createdAt: "DESC" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Brackets, IsNull, Repository } from "typeorm";
|
||||
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { BlogListSearchQueryDto, BlogSearchQueryDto } from "../DTO/search-blog-query.dto";
|
||||
import { Blog } from "../entities/blog.entity";
|
||||
|
||||
@Injectable()
|
||||
export class BlogsRepository extends Repository<Blog> {
|
||||
constructor(@InjectRepository(Blog) blogsRepository: Repository<Blog>) {
|
||||
super(blogsRepository.target, blogsRepository.manager, blogsRepository.queryRunner);
|
||||
}
|
||||
|
||||
async findOneById(id: string): Promise<Blog | null> {
|
||||
return this.findOne({
|
||||
where: { id, deletedAt: IsNull() },
|
||||
relations: {
|
||||
author: true,
|
||||
category: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
previewContent: true,
|
||||
metaTitle: true,
|
||||
metaDescription: true,
|
||||
imageUrl: true,
|
||||
slug: true,
|
||||
createdAt: true,
|
||||
content: true,
|
||||
tags: true,
|
||||
category: {
|
||||
id: true,
|
||||
title: true,
|
||||
iconUrl: true,
|
||||
description: true,
|
||||
},
|
||||
author: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOneBySlug(slug: string): Promise<Blog | null> {
|
||||
return this.findOne({
|
||||
where: { slug, deletedAt: IsNull(), isActive: true },
|
||||
});
|
||||
}
|
||||
|
||||
async fineOneByTitle(title: string): Promise<Blog | null> {
|
||||
return this.findOne({
|
||||
where: { title, deletedAt: IsNull(), isActive: true },
|
||||
});
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getBlogsForUserSide(queryDto: BlogSearchQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.createQueryBuilder("blog")
|
||||
.where("blog.deletedAt IS NULL")
|
||||
.andWhere("blog.isActive = :isActive", { isActive: true })
|
||||
.leftJoinAndSelect("blog.category", "category")
|
||||
.leftJoinAndSelect("blog.author", "author")
|
||||
.select([
|
||||
"blog.id",
|
||||
"blog.title",
|
||||
"blog.previewContent",
|
||||
"blog.metaTitle",
|
||||
"blog.metaDescription",
|
||||
"blog.imageUrl",
|
||||
"blog.slug",
|
||||
"blog.createdAt",
|
||||
"category.id",
|
||||
"category.iconUrl",
|
||||
"category.title",
|
||||
"author.id",
|
||||
"author.firstName",
|
||||
"author.lastName",
|
||||
]);
|
||||
|
||||
if (queryDto.categoryId) {
|
||||
queryBuilder.andWhere("blog.categoryId = :categoryId", { categoryId: queryDto.categoryId });
|
||||
}
|
||||
|
||||
queryBuilder.skip(skip).take(limit).orderBy("blog.createdAt", "DESC");
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
async getBlogListForAdmin(queryDto: BlogListSearchQueryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const queryBuilder = this.createQueryBuilder("blog")
|
||||
.where("blog.deletedAt IS NULL")
|
||||
.leftJoinAndSelect("blog.category", "category")
|
||||
.leftJoinAndSelect("blog.author", "author");
|
||||
|
||||
if (queryDto.isActive) {
|
||||
queryBuilder.andWhere("blog.isActive = :isActive", { isActive: queryDto.isActive === 1 });
|
||||
}
|
||||
|
||||
if (queryDto.q) {
|
||||
// queryBuilder.andWhere(
|
||||
// "blog.title ILIKE :q OR blog.previewContent ILIKE :q OR blog.content ILIKE :q OR blog.metaTitle ILIKE :q OR blog.slug ILIKE :q",
|
||||
// { q: `%${queryDto.q}%` },
|
||||
// );
|
||||
|
||||
queryBuilder.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where("blog.title ILIKE :q", { q: `%${queryDto.q}%` })
|
||||
.orWhere("blog.previewContent ILIKE :q", { q: `%${queryDto.q}%` })
|
||||
.orWhere("blog.content ILIKE :q", { q: `%${queryDto.q}%` })
|
||||
.orWhere("blog.metaTitle ILIKE :q", { q: `%${queryDto.q}%` })
|
||||
.orWhere("blog.slug ILIKE :q", { q: `%${queryDto.q}%` });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (queryDto.categoryId) {
|
||||
queryBuilder.andWhere("blog.categoryId = :categoryId", { categoryId: queryDto.categoryId });
|
||||
}
|
||||
|
||||
if (queryDto.since) {
|
||||
queryBuilder.andWhere("blog.createdAt >= :since", { since: queryDto.since });
|
||||
}
|
||||
|
||||
queryBuilder.skip(skip).take(limit).orderBy("blog.createdAt", "DESC");
|
||||
|
||||
return queryBuilder.getManyAndCount();
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,15 @@ import { ReviewStatus } from "../enums/review-status.enum";
|
||||
|
||||
@Entity()
|
||||
export class DanakServiceReview extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150, nullable: false })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "text", nullable: false })
|
||||
comment: string;
|
||||
|
||||
@Column({ type: "int", nullable: false })
|
||||
rating: number;
|
||||
|
||||
@Column({ type: "varchar", length: 150, nullable: false })
|
||||
title: string;
|
||||
|
||||
@Column({ type: "enum", enum: ReviewStatus, default: ReviewStatus.PENDING })
|
||||
status: ReviewStatus;
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Role } from "./role.entity";
|
||||
import { UserGroup } from "./user-group.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity";
|
||||
import { BlogComment } from "../../blogs/entities/blog-comment.entity";
|
||||
import { Blog } from "../../blogs/entities/blog.entity";
|
||||
import { Criticism } from "../../criticisms/entities/criticism.entity";
|
||||
import { DanakServiceReview } from "../../danak-services/entities/danak-service-review.entity";
|
||||
import { Discount } from "../../discounts/entities/discount.entity";
|
||||
@@ -74,6 +76,12 @@ export class User extends BaseEntity {
|
||||
@OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.user)
|
||||
reviews: DanakServiceReview[];
|
||||
|
||||
@OneToMany(() => Blog, (blog) => blog.author)
|
||||
blogs: Blog[];
|
||||
|
||||
@OneToMany(() => BlogComment, (blogComment) => blogComment.user)
|
||||
blogComments: BlogComment[];
|
||||
|
||||
@OneToMany(() => Ticket, (ticket) => ticket.user)
|
||||
tickets: Ticket[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user