chore: compelete danak category service

This commit is contained in:
mahyargdz
2025-01-26 16:10:12 +03:30
parent e4425a07d6
commit bdd8ec06b4
17 changed files with 367 additions and 16 deletions
+4 -1
View File
@@ -1,4 +1,5 @@
import { Logger } from "@nestjs/common";
import { hash } from "bcrypt";
import { DataSource, DeepPartial } from "typeorm";
import { Role } from "../../src/modules/users/entities/role.entity";
@@ -23,7 +24,9 @@ export const seedAdmin = async (dataSource: DataSource, logger: Logger) => {
const adminRole = await roleRepo.findOneBy({ name: RoleEnum.ADMIN });
if (!adminRole) throw new Error("Role not found");
const admin = userRepo.create({ ...defaultAdmin, role: adminRole });
const hashedPassword = await hash(defaultAdmin.password!, 10);
const admin = userRepo.create({ ...defaultAdmin, password: hashedPassword, role: adminRole });
await userRepo.save(admin);
logger.log("admin created successfully");
} catch (error) {
+22
View File
@@ -0,0 +1,22 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsNotEmpty, IsNumber, IsOptional, Max, Min } from "class-validator";
export class PaginationDto {
@IsOptional()
@IsNotEmpty()
@IsNumber()
@Min(1)
@Type(() => Number)
@ApiPropertyOptional({ type: "number", required: false })
page?: number;
@IsOptional()
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(50)
@Type(() => Number)
@ApiPropertyOptional({ type: "number", required: false })
limit?: number;
}
+11
View File
@@ -0,0 +1,11 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsUUID } from "class-validator";
import { CommonMessage } from "../enums/message.enum";
export class ParamDto {
@IsNotEmpty({ message: CommonMessage.ID_REQUIRED })
@IsUUID("4", { message: CommonMessage.ID_SHOULD_BE_UUID })
@ApiProperty({ description: "Id of the entity", example: "8b1e8b1e-8b1e-8b1e-8b1e-8b1e8b1e8b1e" })
id: string;
}
@@ -0,0 +1,9 @@
import { applyDecorators } from "@nestjs/common";
import { ApiQuery } from "@nestjs/swagger";
export function Pagination() {
return applyDecorators(
ApiQuery({ name: "page", example: 1, required: false, type: "number" }),
ApiQuery({ name: "limit", example: 10, required: false, type: "number" }),
);
}
+36
View File
@@ -68,6 +68,8 @@ export const enum CommonMessage {
VALID_FOR_CHOOSE = "معتبر برای انتخاب",
UPDATE_SUCCESS = "با موفقیت به روز رسانی شد",
CREATED = "با موفقیت ایجاد شد",
ID_REQUIRED = "شناسه مورد نیاز است",
ID_SHOULD_BE_UUID = "شناسه باید یک یو یو آی دی باشد",
}
export const enum CategoryMessage {
@@ -79,6 +81,40 @@ export const enum CategoryMessage {
PARENT_ID_REQUIRED = "شناسه والد دسته بندی مورد نیاز است",
PARENT_ID_SHOULD_BE_UUID = "شناسه والد باید یک یو یو آی دی باشد",
TITLE_EXIST = "دسته بندی با این نام قبلا ثبت شده است",
IS_ACTIVE_REQUIRED = "وضعیت دسته بندی مورد نیاز است",
CAT_ID_SHOULD_BE_UUID = "شناسه دسته بندی باید یک یو یو آی دی باشد",
CAT_ID_NOT_EMPTY = "شناسه دسته بندی نمیتواند خالی باشد",
CATEGORY_NOT_EXIST = "دسته بندی مورد نظر یافت نشد",
IS_ACTIVE_SHOULD_BE_BOOLEAN = "وضعیت دسته بندی باید یک بولین باشد",
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
}
export const enum ServiceMessage {
NAME_REQUIRED = "نام سرویس مورد نیاز است",
NAME_STRING = "نام سرویس باید یک رشته باشد",
NAME_LENGTH = "نام سرویس باید بین ۳ تا ۵۰ کاراکتر باشد",
DESCRIPTION_REQUIRED = "توضیحات سرویس مورد نیاز است",
DESCRIPTION_STRING = "توضیحات سرویس باید یک رشته باشد",
AUTHOR_REQUIRED = "نویسنده سرویس مورد نیاز است",
AUTHOR_STRING = "نویسنده سرویس باید یک رشته باشد",
CREATE_DATE_REQUIRED = "تاریخ ایجاد سرویس مورد نیاز است",
CREATE_DATE_STRING = "تاریخ ایجاد سرویس باید یک رشته باشد",
USER_COUNT_REQUIRED = "تعداد کاربران سرویس مورد نیاز است",
USER_COUNT_INT = "تعداد کاربران سرویس باید یک عدد صحیح باشد",
SERVICE_LANGUAGE_REQUIRED = "زبان سرویس مورد نیاز است",
SOFTWARE_LANGUAGE_REQUIRED = "زبان نرم افزار سرویس مورد نیاز است",
SOFTWARE_LANGUAGE_STRING = "زبان نرم افزار سرویس باید یک رشته باشد",
META_DESCRIPTION_REQUIRED = "متا توضیحات سرویس مورد نیاز است",
LINK_REQUIRED = "لینک سرویس مورد نیاز است",
IS_DANAK_SUGGEST_REQUIRED = "آیا سرویس پیشنهادی داناک است؟",
IS_DANAK_SUGGEST_BOOLEAN = "وضعیت سرویس پیشنهادی داناک باید یک بولین باشد",
ICON_REQUIRED = "آیکون سرویس مورد نیاز است",
LINK_SHOULD_BE_URL = "لینک باید یک آدرس یو آر ال باشد",
ICON_SHOULD_BE_URL = "آیکون باید یک آدرس یو آر ال باشد",
IMAGES_REQUIRED = "تصاویر سرویس مورد نیاز است",
DESCRIPTION_LENGTH = "توضیحات سرویس باید بین ۱۰ تا ۵۰۰ کاراکتر باشد",
SOFTWARE_LANGUAGE_LENGTH = "زبان نرم افزار سرویس باید بین ۳ تا ۱۵۰ کاراکتر باشد",
AUTHOR_LENGTH = "نویسنده سرویس باید بین 3 تا 100 کاراکتر باشد",
}
export const enum AnnouncementMessage {
+1 -1
View File
@@ -19,7 +19,7 @@ export class RoleGuard implements CanActivate {
if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
const hasRequiredRole = requiredRole.includes(user.role.name);
const hasRequiredRole = requiredRole.includes(user.role as unknown as RoleEnum);
if (!hasRequiredRole) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
return true;
+4 -1
View File
@@ -98,11 +98,14 @@ export class AuthService {
}
//****************** */
async requestLoginOtp(requestOtpDto: RequestOtpDto) {
async requestLoginOtp(requestOtpDto: RequestOtpDto, isAdmin: boolean = false) {
const { phone } = requestOtpDto;
const user = await this.usersService.findOneWithPhone(phone);
if (!user) throw new BadRequestException(AuthMessage.PHONE_NOT_FOUND);
//check the if the method call is from admin or not
if (isAdmin && user.role.name !== RoleEnum.ADMIN) throw new BadRequestException(AuthMessage.NOT_ADMIN);
const existCode = await this.otpService.checkExistOtp(phone, "LOGIN");
if (existCode) {
return {
@@ -0,0 +1,26 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsOptional, IsString, IsUUID } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { CategoryMessage } from "../../../common/enums/message.enum";
export class CategorySearchQueryDto {
@IsOptional()
@IsUUID("4", { message: CategoryMessage.PARENT_ID_SHOULD_BE_UUID })
@ApiPropertyOptional({ description: "Parent category id", example: "8b1e8b1e-8b1e-8b1e-8b1e-8b1e8b1e8b1e" })
parentId?: string;
}
export class CategoryListSearchQueryDto extends PaginationDto {
@IsOptional()
@Type(() => Number)
// @IsBoolean({ message: CategoryMessage.IS_ACTIVE_SHOULD_BE_BOOLEAN })
@ApiPropertyOptional({ description: "Category status", example: 1 })
isActive?: number;
@IsOptional()
@IsString({ message: CategoryMessage.SEARCH_QUERY_STRING })
@ApiPropertyOptional({ description: "Search query", example: "search query" })
q?: string;
}
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length } from "class-validator";
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length } from "class-validator";
import { CategoryMessage } from "../../../common/enums/message.enum";
@@ -15,6 +15,11 @@ export class CreateCategoryDto {
@ApiProperty({ description: "Category icon", example: "https://example.com/icon.png" })
icon: string;
@IsNotEmpty({ message: CategoryMessage.IS_ACTIVE_REQUIRED })
@IsBoolean({ message: CategoryMessage.IS_ACTIVE_REQUIRED })
@ApiProperty({ description: "Category status", example: true })
isActive: boolean;
@IsOptional()
@IsNotEmpty({ message: CategoryMessage.PARENT_ID_REQUIRED })
@IsUUID("4", { message: CategoryMessage.PARENT_ID_SHOULD_BE_UUID })
@@ -0,0 +1,76 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsDateString, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, IsUrl, Length, MaxLength } from "class-validator";
import { CategoryMessage, ServiceMessage } from "../../../common/enums/message.enum";
import { ServicesLanguage } from "../enums/services-language.enum";
export class CreateServiceDto {
@IsNotEmpty({ message: CategoryMessage.CAT_ID_NOT_EMPTY })
@IsUUID("4", { message: CategoryMessage.CAT_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "The category id of the service", example: "00000000-0000-0000-0000-000000000000" })
categoryId: string;
@IsNotEmpty({ message: ServiceMessage.NAME_REQUIRED })
@IsString({ message: ServiceMessage.NAME_STRING })
@Length(3, 50, { message: ServiceMessage.NAME_LENGTH })
@ApiProperty({ description: "The name of the service", example: "Service Name" })
name: string;
@IsNotEmpty({ message: ServiceMessage.DESCRIPTION_REQUIRED })
@IsString({ message: ServiceMessage.DESCRIPTION_STRING })
@Length(10, 500, { message: ServiceMessage.DESCRIPTION_LENGTH })
@ApiProperty({ description: "The description of the service", example: "Service Description" })
description: string;
@IsNotEmpty({ message: ServiceMessage.AUTHOR_REQUIRED })
@IsString({ message: ServiceMessage.AUTHOR_STRING })
@Length(3, 100, { message: ServiceMessage.AUTHOR_LENGTH })
@ApiProperty({ description: "The author of the service", example: "danak corp" })
author: string;
@IsNotEmpty({ message: ServiceMessage.CREATE_DATE_REQUIRED })
@IsDateString({}, { message: ServiceMessage.CREATE_DATE_STRING })
@ApiProperty({ description: "The create date of the service", example: "2021-08-01T00:00:00.000Z" })
createDate: Date;
@IsNotEmpty({ message: ServiceMessage.USER_COUNT_REQUIRED })
@IsInt({ message: ServiceMessage.USER_COUNT_INT })
@ApiProperty({ description: "The user count of the service", example: 1000 })
userCount: number;
@IsNotEmpty({ message: ServiceMessage.SERVICE_LANGUAGE_REQUIRED })
@IsEnum(ServicesLanguage)
@ApiProperty({ enum: ServicesLanguage, description: "The language of the service", example: ServicesLanguage.ENGLISH })
serviceLanguage: ServicesLanguage;
@IsNotEmpty({ message: ServiceMessage.SOFTWARE_LANGUAGE_REQUIRED })
@IsString({ message: ServiceMessage.SOFTWARE_LANGUAGE_STRING })
@MaxLength(150, { message: ServiceMessage.SOFTWARE_LANGUAGE_LENGTH })
@ApiProperty({ description: "The software language of the service", example: "Python" })
softwareLanguage: string;
@IsNotEmpty({ message: ServiceMessage.META_DESCRIPTION_REQUIRED })
@IsString({ message: ServiceMessage.META_DESCRIPTION_REQUIRED })
@ApiProperty({ description: "The meta description of the service", example: "Service Meta Description" })
metaDescription: string;
@IsNotEmpty({ message: ServiceMessage.LINK_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.LINK_SHOULD_BE_URL })
@ApiProperty({ description: "The status of the service", example: "https://example.com" })
link: string;
@IsNotEmpty({ message: ServiceMessage.IS_DANAK_SUGGEST_REQUIRED })
@IsBoolean({ message: ServiceMessage.IS_DANAK_SUGGEST_BOOLEAN })
@ApiProperty({ description: "Is the service a Danak suggest?", example: true })
isDanakSuggest: boolean;
@IsNotEmpty({ message: ServiceMessage.ICON_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.ICON_SHOULD_BE_URL })
@ApiProperty({ description: "The icon url of the service", example: "https://example.com/icon.png" })
icon: string;
@IsNotEmpty({ message: ServiceMessage.IMAGES_REQUIRED })
@IsString({ each: true })
@ApiProperty({ description: "The images of the service", example: ["https://example.com/image1.png", "https://example.com/image2.png"] })
images: string[];
}
@@ -1,18 +1,61 @@
import { Body, Controller, Post } from "@nestjs/common";
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { CategoryListSearchQueryDto, CategorySearchQueryDto } from "./DTO/category-search-query.dto";
import { CreateCategoryDto } from "./DTO/create-category.dto";
import { CreateServiceDto } from "./DTO/create-service.dto";
import { DanakServicesService } from "./providers/danak-services.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { Pagination } from "../../common/decorators/pagination.decorator";
import { Roles } from "../../common/decorators/roles.decorator";
import { ParamDto } from "../../common/DTO/param.dto";
import { RoleEnum } from "../users/enums/role.enum";
@Controller("services")
@ApiTags("Danak-Services")
export class DanakServicesController {
constructor(private readonly danakServicesService: DanakServicesService) {}
//
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@ApiOperation({ summary: "Create a new service category" })
@Post("category")
createCategory(@Body() createDto: CreateCategoryDto) {
return this.danakServicesService.createCategory(createDto);
}
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@ApiOperation({ summary: "Get all service categories" })
@Get("category")
getCategories(@Query() queryDto: CategorySearchQueryDto) {
return this.danakServicesService.getCategories(queryDto);
}
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@ApiOperation({ summary: "Get all service categories" })
@Pagination()
@Get("category-list")
getCategoryList(@Query() queryDto: CategoryListSearchQueryDto) {
return this.danakServicesService.getCategoryList(queryDto);
}
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@ApiOperation({ summary: "toggle status of categories" })
@HttpCode(HttpStatus.OK)
@Post("category/toggle-status/:id")
toggleCategoryStatus(@Param() paramDto: ParamDto) {
return this.danakServicesService.toggleCategoryStatus(paramDto);
}
@AuthGuards()
@Roles(RoleEnum.ADMIN)
@ApiOperation({ summary: "create new danak services" })
@Post()
createService(@Body() createDto: CreateServiceDto) {
return this.danakServicesService.createService(createDto);
}
}
@@ -11,6 +11,9 @@ export class DanakServiceCategory extends BaseEntity {
@Column({ type: "varchar", length: 150, nullable: false })
icon: string;
@Column({ type: "boolean", default: true })
isActive: boolean;
@ManyToOne(() => DanakServiceCategory, (category) => category.children)
@JoinColumn({ name: "parentId" })
parent: DanakServiceCategory;
@@ -19,7 +22,7 @@ export class DanakServiceCategory extends BaseEntity {
children: DanakServiceCategory[];
@Column({ nullable: true })
parentId: string;
parentId: string | null;
@OneToMany(() => DanakService, (danakService) => danakService.category)
danakService: DanakService[];
@@ -8,9 +8,12 @@ import { ServicesLanguage } from "../enums/services-language.enum";
@Entity()
export class DanakService extends BaseEntity {
@Column({ type: "varchar", length: 255, nullable: false })
@Column({ type: "varchar", length: 100, nullable: false })
name: string;
@Column({ type: "boolean", nullable: false, default: false })
isDanakSuggest: boolean;
@Column({ type: "text", nullable: false })
description: string;
@@ -41,7 +44,7 @@ export class DanakService extends BaseEntity {
@Column({ type: "varchar", length: 255, nullable: false })
icon: string;
@OneToMany(() => DanakServiceImage, (danakServiceImage) => danakServiceImage.danakService, { eager: true })
@OneToMany(() => DanakServiceImage, (danakServiceImage) => danakServiceImage.danakService, { eager: true, cascade: true })
images: DanakServiceImage[];
@ManyToOne(() => DanakServiceCategory, (danakServiceCategory) => danakServiceCategory.danakService, {
@@ -1,17 +1,27 @@
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { BadRequestException, Injectable } from "@nestjs/common";
import { FindOptionsWhere, IsNull } from "typeorm";
import { ParamDto } from "../../../common/DTO/param.dto";
import { CategoryMessage, CommonMessage } from "../../../common/enums/message.enum";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { CategoryListSearchQueryDto, CategorySearchQueryDto } from "../DTO/category-search-query.dto";
import { CreateCategoryDto } from "../DTO/create-category.dto";
import { CreateServiceDto } from "../DTO/create-service.dto";
import { DanakServiceCategory } from "../entities/danak-service-category.entity";
import { DanakServicesCategoryRepository } from "../repositories/danak-services-category.repository";
import { DanakServicesRepository } from "../repositories/danak-services.repository";
@Injectable()
export class DanakServicesService {
private readonly logger = new Logger(DanakServicesService.name);
constructor(private readonly danakServicesCategoryRepository: DanakServicesCategoryRepository) {}
/********** */
// private readonly logger = new Logger(DanakServicesService.name);
constructor(
private readonly danakServicesRepository: DanakServicesRepository,
private readonly danakServicesCategoryRepository: DanakServicesCategoryRepository,
) {}
/******************************************** */
async createCategory(createDto: CreateCategoryDto) {
this.logger.log(`Creating a new category with title: ${createDto.title}`);
const existCategory = await this.danakServicesCategoryRepository.findOneBy({ title: createDto.title });
const existCategory = await this.danakServicesCategoryRepository.findOneByTitle(createDto.title);
if (existCategory) throw new BadRequestException(CategoryMessage.TITLE_EXIST);
const category = this.danakServicesCategoryRepository.create(createDto);
await this.danakServicesCategoryRepository.save(category);
@@ -20,5 +30,81 @@ export class DanakServicesService {
category,
};
}
/********** */
/******************************************** */
async getCategories(queryDto: CategorySearchQueryDto) {
const where: FindOptionsWhere<DanakServiceCategory> = {
isActive: true,
};
if (queryDto.parentId === undefined) {
where.parentId = IsNull();
} else {
// If parentId is provided, use it
where.parentId = queryDto.parentId;
}
const categories = await this.danakServicesCategoryRepository.find({
where,
order: { createdAt: "ASC" },
});
return { categories };
}
/******************************************** */
async getCategoryList(queryDto: CategoryListSearchQueryDto) {
const { limit, skip } = PaginationUtils(queryDto);
const queryBuilder = this.danakServicesCategoryRepository
.createQueryBuilder("category")
.loadRelationCountAndMap("category.servicesCount", "category.danakService")
.loadRelationCountAndMap("category.childrenCount", "category.children");
if (queryDto.isActive !== undefined) {
queryBuilder.andWhere("category.isActive = :isActive", { isActive: queryDto.isActive === 1 });
}
if (queryDto.q) {
queryBuilder.andWhere("category.title LIKE :q", { q: `%${queryDto.q}%` });
}
queryBuilder
.leftJoinAndSelect("category.parent", "parent")
.select(["category", "parent.id", "parent.title"])
.orderBy("category.createdAt", "DESC");
const [categories, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount();
return { categories, count };
}
/******************************************** */
async toggleCategoryStatus(paramDto: ParamDto) {
const category = await this.danakServicesCategoryRepository.findOneById(paramDto.id);
if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST);
category.isActive = !category.isActive;
await this.danakServicesCategoryRepository.save(category);
return {
message: CommonMessage.UPDATE_SUCCESS,
isActive: category.isActive,
};
}
/******************************************** */
async createService(createDto: CreateServiceDto) {
const category = await this.danakServicesCategoryRepository.findOneById(createDto.categoryId);
if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST);
const images = createDto.images.map((image) => {
return { imageUrl: image };
});
const service = this.danakServicesRepository.create({ ...createDto, category, images });
await this.danakServicesRepository.save(service);
return {
message: CommonMessage.CREATED,
service,
};
}
/******************************************** */
/******************************************** */
}
@@ -9,4 +9,12 @@ export class DanakServicesCategoryRepository extends Repository<DanakServiceCate
constructor(@InjectRepository(DanakServiceCategory) danakServicesCategoryRepository: Repository<DanakServiceCategory>) {
super(danakServicesCategoryRepository.target, danakServicesCategoryRepository.manager, danakServicesCategoryRepository.queryRunner);
}
async findOneByTitle(title: string): Promise<DanakServiceCategory | null> {
return this.findOneBy({ title });
}
async findOneById(id: string): Promise<DanakServiceCategory | null> {
return this.findOneBy({ id });
}
}
+1 -1
View File
@@ -2,7 +2,6 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import slugify from "slugify";
import { In, Not } from "typeorm";
import { RoleRepository } from "../repositories/roles.repository";
// import { roles } from "../../../../database/seeders/role.seeder";
import { CommonMessage, UserMessage } from "../../../common/enums/message.enum";
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
@@ -12,6 +11,7 @@ import { CreateUserGroupDto } from "../DTO/user-group.dto";
import { User } from "../entities/user.entity";
import { RoleEnum } from "../enums/role.enum";
import { ValidityType } from "../enums/validity-type.enum";
import { RoleRepository } from "../repositories/roles.repository";
import { UserRepository } from "../repositories/users.repository";
@Injectable()
@@ -0,0 +1,17 @@
import { PaginationDto } from "../../../common/DTO/pagination.dto";
export function PaginationUtils(paginationData: PaginationDto) {
const { limit = 10, page = 1 } = paginationData;
// const pageN = Number.parseInt(page) || 1;
// const limitN = Number.parseInt(limit) || 10;
const skip = (page - 1) * limit;
return {
// page: page,
// limit: limit > 50 ? 50 : limit,
limit: limit,
skip: skip,
};
}