From 2292c0122047faf17fae675bf53a7b81a9e8de6e Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Tue, 15 Oct 2024 11:01:56 +0400 Subject: [PATCH] community controller --- libs/common/src/constants/controller-route.ts | 30 ++ libs/common/src/database/database.module.ts | 2 + libs/common/src/dto/base.response.dto.ts | 23 ++ libs/common/src/dto/pagination.request.dto.ts | 87 +++++ .../common/src/dto/pagination.response.dto.ts | 62 +++ libs/common/src/dto/success.response.dto.ts | 29 ++ libs/common/src/models/typeOrmCustom.model.ts | 145 +++++++ .../modules/community/dtos/community.dto.ts | 19 + .../src/modules/community/dtos/index.ts | 1 + .../community/entities/community.entity.ts | 38 ++ .../src/modules/community/entities/index.ts | 1 + .../repositories/community.repository.ts | 10 + .../modules/community/repositories/index.ts | 1 + libs/common/src/type/optional.type.ts | 2 + libs/common/src/util/parseToDate.ts | 4 + .../is-page-request-param.validator.ts | 21 ++ .../is-size-request-param.validator.ts | 21 ++ .../src/validators/is-sort-param.validator.ts | 54 +++ .../controllers/community.controller.ts | 134 ++++--- src/community/dtos/add.community.dto.ts | 25 +- src/community/dtos/get.community.dto.ts | 13 + src/community/dtos/update.community.dto.ts | 4 +- src/community/services/community.service.ts | 355 ++++++++---------- 23 files changed, 822 insertions(+), 259 deletions(-) create mode 100644 libs/common/src/dto/base.response.dto.ts create mode 100644 libs/common/src/dto/pagination.request.dto.ts create mode 100644 libs/common/src/dto/pagination.response.dto.ts create mode 100644 libs/common/src/dto/success.response.dto.ts create mode 100644 libs/common/src/models/typeOrmCustom.model.ts create mode 100644 libs/common/src/modules/community/dtos/community.dto.ts create mode 100644 libs/common/src/modules/community/dtos/index.ts create mode 100644 libs/common/src/modules/community/entities/community.entity.ts create mode 100644 libs/common/src/modules/community/entities/index.ts create mode 100644 libs/common/src/modules/community/repositories/community.repository.ts create mode 100644 libs/common/src/modules/community/repositories/index.ts create mode 100644 libs/common/src/type/optional.type.ts create mode 100644 libs/common/src/util/parseToDate.ts create mode 100644 libs/common/src/validators/is-page-request-param.validator.ts create mode 100644 libs/common/src/validators/is-size-request-param.validator.ts create mode 100644 libs/common/src/validators/is-sort-param.validator.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index a1e43ee..5e643c8 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -8,4 +8,34 @@ export class ControllerRoute { 'Retrieve the list of all regions registered in Syncrow.'; }; }; + + static COMMUNITY = class { + public static readonly ROUTE = 'communities'; + static ACTIONS = class { + public static readonly GET_COMMUNITY_BY_ID_SUMMARY = + 'Get community by community id'; + + public static readonly GET_COMMUNITY_BY_ID_DESCRIPTION = + 'Get community by community id - ( [a-zA-Z0-9]{10} )'; + + public static readonly LIST_COMMUNITY_SUMMARY = 'Get list of community'; + + public static readonly LIST_COMMUNITY_DESCRIPTION = + 'Return a list of community'; + public static readonly UPDATE_COMMUNITY_SUMMARY = 'Update community'; + + public static readonly UPDATE_COMMUNITY_DESCRIPTION = + 'Update community in the database and return updated community'; + + public static readonly DELETE_COMMUNITY_SUMMARY = 'Delete community'; + + public static readonly DELETE_COMMUNITY_DESCRIPTION = + 'Delete community matching by community id'; + + public static readonly CREATE_COMMUNITY_SUMMARY = 'Create community'; + + public static readonly CREATE_COMMUNITY_DESCRIPTION = + 'Create community in the database and return in model'; + }; + }; } diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index ae01e55..deaa021 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -19,6 +19,7 @@ import { DeviceNotificationEntity } from '../modules/device/entities'; import { RegionEntity } from '../modules/region/entities'; import { TimeZoneEntity } from '../modules/timezone/entities'; import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; +import { CommunityEntity } from '../modules/community/entities'; @Module({ imports: [ @@ -41,6 +42,7 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities'; DeviceUserPermissionEntity, DeviceEntity, PermissionTypeEntity, + CommunityEntity, SpaceEntity, SpaceTypeEntity, UserSpaceEntity, diff --git a/libs/common/src/dto/base.response.dto.ts b/libs/common/src/dto/base.response.dto.ts new file mode 100644 index 0000000..c51d38c --- /dev/null +++ b/libs/common/src/dto/base.response.dto.ts @@ -0,0 +1,23 @@ +import { WithOptional } from '../type/optional.type'; + +export class BaseResponseDto { + statusCode?: number; + + message: string; + + error?: string; + + data?: any; + + success?: boolean; + + static wrap({ + data, + statusCode = 200, + message = 'Success', + success = true, + error = undefined, + }: WithOptional) { + return { data, statusCode, success, message, error }; + } +} diff --git a/libs/common/src/dto/pagination.request.dto.ts b/libs/common/src/dto/pagination.request.dto.ts new file mode 100644 index 0000000..4eff373 --- /dev/null +++ b/libs/common/src/dto/pagination.request.dto.ts @@ -0,0 +1,87 @@ +import { IsDate, IsOptional } from 'class-validator'; +import { IsPageRequestParam } from '../validators/is-page-request-param.validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsSizeRequestParam } from '../validators/is-size-request-param.validator'; +import { IsSortParam } from '../validators/is-sort-param.validator'; +import { Transform } from 'class-transformer'; +import { parseToDate } from '../util/parseToDate'; + +export class PaginationRequestGetListDto { + @IsOptional() + @IsPageRequestParam({ + message: 'Page must be bigger than 0', + }) + @ApiProperty({ + name: 'page', + required: false, + description: 'Page request', + }) + page?: number; + + @IsOptional() + @IsSizeRequestParam({ + message: 'Size must not be negative', + }) + @ApiProperty({ + name: 'size', + required: false, + description: 'Size request', + }) + size?: number; + + @IsOptional() + @IsSortParam({ + message: + 'Incorrect sorting condition format. Should be like this format propertyId:asc,createdDate:desc', + }) + @ApiProperty({ + name: 'sort', + required: false, + description: 'Sort condition', + }) + sort?: string; + + @IsOptional() + @ApiProperty({ + name: 'name', + required: false, + description: 'Name to be filtered', + }) + name?: string; + + @IsOptional() + @ApiProperty({ + name: 'include', + required: false, + description: 'Fields to include', + }) + include?: string; + + @ApiProperty({ + name: 'from', + required: false, + type: Number, + description: `Start time in UNIX timestamp format to filter`, + example: 1674172800000, + }) + @IsOptional() + @Transform(({ value }) => parseToDate(value)) + @IsDate({ + message: `From must be in UNIX timestamp format in order to parse to Date instance`, + }) + from?: Date; + + @ApiProperty({ + name: 'to', + required: false, + type: Number, + description: `End time in UNIX timestamp format to filter`, + example: 1674259200000, + }) + @IsOptional() + @Transform(({ value }) => parseToDate(value)) + @IsDate({ + message: `To must be in UNIX timestamp format in order to parse to Date instance`, + }) + to?: Date; +} diff --git a/libs/common/src/dto/pagination.response.dto.ts b/libs/common/src/dto/pagination.response.dto.ts new file mode 100644 index 0000000..5a0e4cf --- /dev/null +++ b/libs/common/src/dto/pagination.response.dto.ts @@ -0,0 +1,62 @@ +import { BaseResponseDto } from './base.response.dto'; + +export interface PageResponseDto { + // Original paging information from the request ( or default ) + page: number; + size: number; + + // Useful for display (N Records found) + totalItem: number; + + // Use for display N Pages ( 0... N ) + totalPage: number; + + // Has next is false when cursor is at last page + hasNext: boolean; + + // Has previous is false when cursor is at first page + hasPrevious: boolean; +} + +export class PageResponse implements BaseResponseDto, PageResponseDto { + code?: number; + + message: string; + + data: Array; + + page: number; + + size: number; + + totalItem: number; + + totalPage: number; + + hasNext: boolean; + + hasPrevious: boolean; + + constructor( + baseResponseDto: BaseResponseDto, + pageResponseDto: PageResponseDto, + ) { + if (baseResponseDto.statusCode) { + this.code = baseResponseDto.statusCode; + } else { + this.code = 200; + } + + if (baseResponseDto.data) { + this.data = baseResponseDto.data; + } + + this.message = baseResponseDto.message; + this.page = pageResponseDto.page; + this.size = pageResponseDto.size; + this.totalItem = pageResponseDto.totalItem; + this.totalPage = pageResponseDto.totalPage; + this.hasNext = pageResponseDto.hasNext; + this.hasPrevious = pageResponseDto.hasPrevious; + } +} diff --git a/libs/common/src/dto/success.response.dto.ts b/libs/common/src/dto/success.response.dto.ts new file mode 100644 index 0000000..f21bf78 --- /dev/null +++ b/libs/common/src/dto/success.response.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { BaseResponseDto } from './base.response.dto'; + +export class SuccessResponseDto implements BaseResponseDto { + @ApiProperty({ + example: 200, + }) + statusCode: number; + + data: Type; + + @ApiProperty({ + example: 'Success message', + }) + message: string; + + @ApiProperty({ + example: true, + description: 'Indicates that the operation was successful', + }) + success: boolean; + + constructor(input: BaseResponseDto) { + if (input.statusCode) this.statusCode = input.statusCode; + else this.statusCode = 200; + if (input.data) this.data = input.data; + this.success = true; + } +} diff --git a/libs/common/src/models/typeOrmCustom.model.ts b/libs/common/src/models/typeOrmCustom.model.ts new file mode 100644 index 0000000..b1e102a --- /dev/null +++ b/libs/common/src/models/typeOrmCustom.model.ts @@ -0,0 +1,145 @@ +import { FindManyOptions, Repository } from 'typeorm'; +import { InternalServerErrorException } from '@nestjs/common'; +import { BaseResponseDto } from '../dto/base.response.dto'; +import { PageResponseDto } from '../dto/pagination.response.dto'; +import { buildTypeORMSortQuery } from '../util/buildTypeORMSortQuery'; +import { getPaginationResponseDto } from '../util/getPaginationResponseDto'; +import { buildTypeORMWhereClause } from '../util/buildTypeORMWhereClause'; +import { buildTypeORMIncludeQuery } from '../util/buildTypeORMIncludeQuery'; + +export interface TypeORMCustomModelFindAllQuery { + page: number | undefined; + size: number | undefined; + sort?: string; + modelName?: string; + include?: string; + where?: { [key: string]: unknown }; + select?: string[]; + includeDisable?: boolean | string; +} +interface CustomFindAllQuery { + page?: number; + size?: number; + [key: string]: any; +} + +interface FindAllQueryWithDefaults extends CustomFindAllQuery { + page: number; + size: number; +} + +function getDefaultQueryOptions( + query: Partial, +): FindManyOptions & FindAllQueryWithDefaults { + const { page, size, includeDisable, modelName, ...rest } = query; + + // Set default if undefined or null + const returnPage = page ? Number(page) : 1; + const returnSize = size ? Number(size) : 10; + const returnIncludeDisable = + includeDisable === true || includeDisable === 'true'; + + // Return query with defaults and ensure modelName is passed through + return { + skip: (returnPage - 1) * returnSize, + take: returnSize, + where: { + ...rest, + }, + page: returnPage, + size: returnSize, + includeDisable: returnIncludeDisable, + modelName: modelName || query.modelName, // Ensure modelName is passed through + }; +} + +export interface TypeORMCustomModelFindAllQueryWithDefault + extends TypeORMCustomModelFindAllQuery { + page: number; + size: number; +} + +export type TypeORMCustomModelFindAllResponse = { + baseResponseDto: BaseResponseDto; + paginationResponseDto: PageResponseDto; +}; + +export function TypeORMCustomModel(repository: Repository) { + return Object.assign(repository, { + findAll: async function ( + query: Partial, + ): Promise { + // Extract values from the query + const { + page = 1, + size = 10, + sort, + modelName, + include, + where, + select, + } = getDefaultQueryOptions(query); + + // Ensure modelName is set before proceeding + if (!modelName) { + console.error( + 'modelName is missing after getDefaultQueryOptions:', + query, + ); + throw new InternalServerErrorException( + `[TypeORMCustomModel] Cannot findAll with unknown modelName`, + ); + } + + const skip = (page - 1) * size; + const order = buildTypeORMSortQuery(sort); + const relations = buildTypeORMIncludeQuery(modelName, include); + + // Use the where clause directly, without wrapping it under 'where' + const whereClause = buildTypeORMWhereClause({ where }); + console.log('Where clause after building:', whereClause); + + // Ensure the whereClause is passed directly to findAndCount + const [data, count] = await repository.findAndCount({ + where: whereClause.where || whereClause, // Don't wrap this under another 'where' + take: size, + skip: skip, + order: order, + select: select, + relations: relations, + }); + + const paginationResponseDto = getPaginationResponseDto(count, page, size); + const baseResponseDto: BaseResponseDto = { + data, + message: getResponseMessage(modelName, { where: whereClause }), + }; + + return { baseResponseDto, paginationResponseDto }; + }, + }); +} + +function getResponseMessage( + modelName: string, + query?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + where?: any; + }, +): string { + if (!query) { + return `Success get list ${modelName}`; + } + + const { where } = query; + if (modelName === 'user' && where && where?.community) { + const { + some: { communityId }, + } = where.community; + if (typeof communityId === 'string') { + return `Success get list ${modelName} belong to community`; + } + } + + return `Success get list ${modelName}`; +} diff --git a/libs/common/src/modules/community/dtos/community.dto.ts b/libs/common/src/modules/community/dtos/community.dto.ts new file mode 100644 index 0000000..ebafb7e --- /dev/null +++ b/libs/common/src/modules/community/dtos/community.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; + +export class CommunityDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public name: string; + + @IsString() + @IsOptional() + public description?: string; + + @IsUUID() + @IsNotEmpty() + public regionId: string; +} diff --git a/libs/common/src/modules/community/dtos/index.ts b/libs/common/src/modules/community/dtos/index.ts new file mode 100644 index 0000000..e030f3a --- /dev/null +++ b/libs/common/src/modules/community/dtos/index.ts @@ -0,0 +1 @@ +export * from './community.dto'; diff --git a/libs/common/src/modules/community/entities/community.entity.ts b/libs/common/src/modules/community/entities/community.entity.ts new file mode 100644 index 0000000..1474f05 --- /dev/null +++ b/libs/common/src/modules/community/entities/community.entity.ts @@ -0,0 +1,38 @@ +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { CommunityDto } from '../dtos'; +import { RegionEntity } from '../../region/entities'; +import { SpaceEntity } from '../../space/entities'; +import { RoleEntity } from '../../role/entities'; + +@Entity({ name: 'community' }) +@Unique(['name']) +export class CommunityEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + length: 255, + nullable: false, + }) + name: string; + + @Column({ length: 255, nullable: true }) + description: string; + + @ManyToOne(() => RegionEntity, (region) => region.communities, { + nullable: false, + onDelete: 'CASCADE', + }) + region: RegionEntity; + + @OneToMany(() => SpaceEntity, (space) => space.community) + spaces: SpaceEntity[]; + + @OneToMany(() => RoleEntity, (role) => role.community) + roles: RoleEntity[]; +} diff --git a/libs/common/src/modules/community/entities/index.ts b/libs/common/src/modules/community/entities/index.ts new file mode 100644 index 0000000..61f1d4c --- /dev/null +++ b/libs/common/src/modules/community/entities/index.ts @@ -0,0 +1 @@ +export * from './community.entity'; diff --git a/libs/common/src/modules/community/repositories/community.repository.ts b/libs/common/src/modules/community/repositories/community.repository.ts new file mode 100644 index 0000000..137ef2e --- /dev/null +++ b/libs/common/src/modules/community/repositories/community.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { CommunityEntity } from '../entities'; + +@Injectable() +export class CommunityRepository extends Repository { + constructor(private dataSource: DataSource) { + super(CommunityEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/community/repositories/index.ts b/libs/common/src/modules/community/repositories/index.ts new file mode 100644 index 0000000..fb4a11d --- /dev/null +++ b/libs/common/src/modules/community/repositories/index.ts @@ -0,0 +1 @@ +export * from './community.repository'; diff --git a/libs/common/src/type/optional.type.ts b/libs/common/src/type/optional.type.ts new file mode 100644 index 0000000..cf62d1c --- /dev/null +++ b/libs/common/src/type/optional.type.ts @@ -0,0 +1,2 @@ +export type WithOptional = Omit & Partial; +export type WithRequired = Omit & Required; diff --git a/libs/common/src/util/parseToDate.ts b/libs/common/src/util/parseToDate.ts new file mode 100644 index 0000000..aa642a1 --- /dev/null +++ b/libs/common/src/util/parseToDate.ts @@ -0,0 +1,4 @@ +export function parseToDate(value: unknown): Date { + const valueInNumber = Number(value); + return new Date(valueInNumber); +} diff --git a/libs/common/src/validators/is-page-request-param.validator.ts b/libs/common/src/validators/is-page-request-param.validator.ts new file mode 100644 index 0000000..a6459e4 --- /dev/null +++ b/libs/common/src/validators/is-page-request-param.validator.ts @@ -0,0 +1,21 @@ +import { ValidateBy, ValidationOptions } from 'class-validator'; + +export function IsPageRequestParam( + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: 'IsPageRequestParam', + validator: { + validate(value) { + return IsPageParam(value); // you can return a Promise here as well, if you want to make async validation + }, + }, + }, + validationOptions, + ); +} + +function IsPageParam(value: any): boolean { + return !isNaN(Number(value)) && value > 0; +} diff --git a/libs/common/src/validators/is-size-request-param.validator.ts b/libs/common/src/validators/is-size-request-param.validator.ts new file mode 100644 index 0000000..4928a1b --- /dev/null +++ b/libs/common/src/validators/is-size-request-param.validator.ts @@ -0,0 +1,21 @@ +import { ValidateBy, ValidationOptions } from 'class-validator'; + +export function IsSizeRequestParam( + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: 'IsSizeRequestParam', + validator: { + validate(value) { + return IsSizeParam(value); // you can return a Promise here as well, if you want to make async validation + }, + }, + }, + validationOptions, + ); +} + +function IsSizeParam(value: any): boolean { + return !isNaN(Number(value)) && value > -1; +} diff --git a/libs/common/src/validators/is-sort-param.validator.ts b/libs/common/src/validators/is-sort-param.validator.ts new file mode 100644 index 0000000..dceee2e --- /dev/null +++ b/libs/common/src/validators/is-sort-param.validator.ts @@ -0,0 +1,54 @@ +import { ValidateBy, ValidationOptions } from 'class-validator'; + +export function IsSortParam( + validationOptions?: ValidationOptions, + allowedFieldName?: string[], +): PropertyDecorator { + return ValidateBy( + { + name: 'IsSortParam', + validator: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + validate(value) { + return IsValidMultipleSortParam(value, allowedFieldName); // you can return a Promise here as well, if you want to make async validation + }, + }, + }, + validationOptions, + ); +} + +function IsValidMultipleSortParam( + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + value: any, + allowedFieldName?: string[], +): boolean { + if (typeof value !== 'string') { + return false; + } + + const conditions: string[] = value.split(','); + const isValid: boolean = conditions.every((condition) => { + const combination: string[] = condition.split(':'); + if (combination.length !== 2) { + return false; + } + const field = combination[0].trim(); + const direction = combination[1].trim(); + + if (!field) { + return false; + } + + if (allowedFieldName?.length && !allowedFieldName.includes(field)) { + return false; + } + + if (!['asc', 'desc'].includes(direction.toLowerCase())) { + return false; + } + + return true; + }); + return isValid; +} diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index 356c90c..d635a80 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -2,7 +2,9 @@ import { CommunityService } from '../services/community.service'; import { Body, Controller, + Delete, Get, + HttpException, HttpStatus, Param, Post, @@ -10,23 +12,25 @@ import { Query, UseGuards, } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { AddCommunityDto, AddUserCommunityDto, } from '../dtos/add.community.dto'; -import { GetCommunityChildDto } from '../dtos/get.community.dto'; +import { GetCommunityParams } from '../dtos/get.community.dto'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; +// import { CheckUserCommunityGuard } from 'src/guards/user.community.guard'; import { AdminRoleGuard } from 'src/guards/admin.role.guard'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; -import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; -import { SpaceType } from '@app/common/constants/space-type.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { PaginationRequestGetListDto } from '@app/common/dto/pagination.request.dto'; // import { CommunityPermissionGuard } from 'src/guards/community.permission.guard'; @ApiTags('Community Module') @Controller({ - version: EnableDisableStatusEnum.ENABLED, - path: SpaceType.COMMUNITY, + version: '1', + path: ControllerRoute.COMMUNITY.ROUTE, }) export class CommunityController { constructor(private readonly communityService: CommunityService) {} @@ -34,73 +38,103 @@ export class CommunityController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post() - async addCommunity(@Body() addCommunityDto: AddCommunityDto) { - const community = await this.communityService.addCommunity(addCommunityDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'Community added successfully', - data: community, - }; + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.CREATE_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.CREATE_COMMUNITY_DESCRIPTION, + }) + async createCommunity( + @Body() addCommunityDto: AddCommunityDto, + ): Promise { + return await this.communityService.createCommunity(addCommunityDto); } @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get(':communityUuid') - async getCommunityByUuid(@Param('communityUuid') communityUuid: string) { - const community = - await this.communityService.getCommunityByUuid(communityUuid); - return community; + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.GET_COMMUNITY_BY_ID_SUMMARY, + description: + ControllerRoute.COMMUNITY.ACTIONS.GET_COMMUNITY_BY_ID_DESCRIPTION, + }) + @Get('/:communityId') + async getCommunityByUuid( + @Param() params: GetCommunityParams, + ): Promise { + return await this.communityService.getCommunityById(params.communityId); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.LIST_COMMUNITY_DESCRIPTION, + }) @Get() - async getCommunities() { - const communities = await this.communityService.getCommunities(); - return communities; + async getCommunities( + @Query() query: PaginationRequestGetListDto, + ): Promise { + return this.communityService.getCommunities(query); } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) - @Get('child/:communityUuid') - async getCommunityChildByUuid( - @Param('communityUuid') communityUuid: string, - @Query() query: GetCommunityChildDto, + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.UPDATE_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.UPDATE_COMMUNITY_DESCRIPTION, + }) + @Put('/:communityId') + async updateCommunity( + @Param() param: GetCommunityParams, + @Body() updateCommunityDto: UpdateCommunityNameDto, ) { - const community = await this.communityService.getCommunityChildByUuid( - communityUuid, - query, + return this.communityService.updateCommunity( + param.communityId, + updateCommunityDto, ); - return community; + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete('/:communityId') + @ApiOperation({ + summary: ControllerRoute.COMMUNITY.ACTIONS.DELETE_COMMUNITY_SUMMARY, + description: ControllerRoute.COMMUNITY.ACTIONS.DELETE_COMMUNITY_DESCRIPTION, + }) + async deleteCommunity( + @Param() param: GetCommunityParams, + ): Promise { + return this.communityService.deleteCommunity(param.communityId); } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('user/:userUuid') async getCommunitiesByUserId(@Param('userUuid') userUuid: string) { - return await this.communityService.getCommunitiesByUserId(userUuid); + try { + return await this.communityService.getCommunitiesByUserId(userUuid); + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } + @ApiBearerAuth() @UseGuards(AdminRoleGuard) @Post('user') async addUserCommunity(@Body() addUserCommunityDto: AddUserCommunityDto) { - await this.communityService.addUserCommunity(addUserCommunityDto); - return { - statusCode: HttpStatus.CREATED, - success: true, - message: 'user community added successfully', - }; - } - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - @Put(':communityUuid') - async renameCommunityByUuid( - @Param('communityUuid') communityUuid: string, - @Body() updateCommunityDto: UpdateCommunityNameDto, - ) { - const community = await this.communityService.renameCommunityByUuid( - communityUuid, - updateCommunityDto, - ); - return community; + try { + await this.communityService.addUserCommunity(addUserCommunityDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user community added successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } } diff --git a/src/community/dtos/add.community.dto.ts b/src/community/dtos/add.community.dto.ts index 06aec7c..bd66d56 100644 --- a/src/community/dtos/add.community.dto.ts +++ b/src/community/dtos/add.community.dto.ts @@ -1,19 +1,38 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; export class AddCommunityDto { @ApiProperty({ - description: 'communityName', + description: 'The name of the community', + example: 'Community A', required: true, }) @IsString() @IsNotEmpty() - public communityName: string; + public name: string; + + @ApiProperty({ + description: 'A description of the community', + example: 'This is a community for developers.', + required: false, + }) + @IsString() + @IsOptional() + public description?: string; + + @ApiProperty({ + description: 'The UUID of the region this community belongs to', + example: 'b6fa62c5-4fcf-4872-89a0-571cf431a6c7', + }) + @IsUUID() + @IsNotEmpty() + public regionId: string; constructor(dto: Partial) { Object.assign(this, dto); } } + export class AddUserCommunityDto { @ApiProperty({ description: 'communityUuid', diff --git a/src/community/dtos/get.community.dto.ts b/src/community/dtos/get.community.dto.ts index cae892b..2b84e19 100644 --- a/src/community/dtos/get.community.dto.ts +++ b/src/community/dtos/get.community.dto.ts @@ -7,6 +7,7 @@ import { IsNotEmpty, IsOptional, IsString, + IsUUID, Min, } from 'class-validator'; @@ -20,6 +21,18 @@ export class GetCommunityDto { public communityUuid: string; } +export class GetCommunityParams { + @ApiProperty({ + description: 'Community id of the specific community', + required: true, + name: 'communityId', + }) + @IsUUID() + @IsString() + @IsNotEmpty() + public communityId: string; +} + export class GetCommunityChildDto { @ApiProperty({ example: 1, description: 'Page number', required: true }) @IsInt({ message: 'Page must be a number' }) diff --git a/src/community/dtos/update.community.dto.ts b/src/community/dtos/update.community.dto.ts index 6f15d43..aba1fc2 100644 --- a/src/community/dtos/update.community.dto.ts +++ b/src/community/dtos/update.community.dto.ts @@ -3,12 +3,12 @@ import { IsNotEmpty, IsString } from 'class-validator'; export class UpdateCommunityNameDto { @ApiProperty({ - description: 'communityName', + description: 'community name', required: true, }) @IsString() @IsNotEmpty() - public communityName: string; + public name: string; constructor(dto: Partial) { Object.assign(this, dto); diff --git a/src/community/services/community.service.ts b/src/community/services/community.service.ts index 145dc13..1cfce8b 100644 --- a/src/community/services/community.service.ts +++ b/src/community/services/community.service.ts @@ -1,183 +1,173 @@ -import { GetCommunityChildDto } from './../dtos/get.community.dto'; -import { SpaceTypeRepository } from './../../../libs/common/src/modules/space/repositories/space.repository'; -import { - Injectable, - HttpException, - HttpStatus, - BadRequestException, -} from '@nestjs/common'; +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; import { AddCommunityDto, AddUserCommunityDto } from '../dtos'; -import { - CommunityChildInterface, - GetCommunitiesInterface, - GetCommunityByUserUuidInterface, - GetCommunityByUuidInterface, - RenameCommunityByUuidInterface, -} from '../interface/community.interface'; -import { SpaceEntity } from '@app/common/modules/space/entities'; +import { GetCommunityByUserUuidInterface } from '../interface/community.interface'; import { UpdateCommunityNameDto } from '../dtos/update.community.dto'; import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { SpaceType } from '@app/common/constants/space-type.enum'; -import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; +import { RegionRepository } from '@app/common/modules/region/repositories'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { + TypeORMCustomModel, + TypeORMCustomModelFindAllQuery, +} from '@app/common/models/typeOrmCustom.model'; +import { PageResponse } from '@app/common/dto/pagination.response.dto'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { CommunityDto } from '@app/common/modules/community/dtos'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; @Injectable() export class CommunityService { constructor( private readonly spaceRepository: SpaceRepository, - private readonly spaceTypeRepository: SpaceTypeRepository, private readonly userSpaceRepository: UserSpaceRepository, + private readonly communityRepository: CommunityRepository, + private readonly regionRepository: RegionRepository, ) {} - async addCommunity(addCommunityDto: AddCommunityDto) { - try { - const spaceType = await this.spaceTypeRepository.findOne({ - where: { - type: SpaceType.COMMUNITY, - }, - }); + async createCommunity(dto: AddCommunityDto): Promise { + const { regionId, name, description } = dto; + // Find the region by the provided regionId + const region = await this.regionRepository.findOneBy({ uuid: regionId }); - const community = await this.spaceRepository.save({ - spaceName: addCommunityDto.communityName, - spaceType: { uuid: spaceType.uuid }, - }); - return community; - } catch (err) { - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - async getCommunityByUuid( - communityUuid: string, - ): Promise { - try { - const community = await this.spaceRepository.findOne({ - where: { - uuid: communityUuid, - spaceType: { - type: SpaceType.COMMUNITY, - }, - }, - relations: ['spaceType'], - }); - if ( - !community || - !community.spaceType || - community.spaceType.type !== SpaceType.COMMUNITY - ) { - throw new BadRequestException('Invalid community UUID'); - } - return { - uuid: community.uuid, - createdAt: community.createdAt, - updatedAt: community.updatedAt, - name: community.spaceName, - type: community.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } - } - } - async getCommunities(): Promise { - try { - const community = await this.spaceRepository.find({ - where: { spaceType: { type: SpaceType.COMMUNITY } }, - relations: ['spaceType'], - }); - return community.map((community) => ({ - uuid: community.uuid, - createdAt: community.createdAt, - updatedAt: community.updatedAt, - name: community.spaceName, - type: community.spaceType.type, - })); - } catch (err) { - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - async getCommunityChildByUuid( - communityUuid: string, - getCommunityChildDto: GetCommunityChildDto, - ): Promise { - try { - const { includeSubSpaces, page, pageSize } = getCommunityChildDto; - - const space = await this.spaceRepository.findOneOrFail({ - where: { uuid: communityUuid }, - relations: ['children', 'spaceType'], - }); - - if ( - !space || - !space.spaceType || - space.spaceType.type !== SpaceType.COMMUNITY - ) { - throw new BadRequestException('Invalid community UUID'); - } - const totalCount = await this.spaceRepository.count({ - where: { parent: { uuid: space.uuid } }, - }); - const children = await this.buildHierarchy( - space, - includeSubSpaces, - page, - pageSize, + if (!region) { + throw new HttpException( + `Region with ID ${dto.regionId} not found.`, + HttpStatus.NOT_FOUND, ); - return { - uuid: space.uuid, - name: space.spaceName, - type: space.spaceType.type, - totalCount, - children, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } } - } - private async buildHierarchy( - space: SpaceEntity, - includeSubSpaces: boolean, - page: number, - pageSize: number, - ): Promise { - const children = await this.spaceRepository.find({ - where: { parent: { uuid: space.uuid } }, - relations: ['spaceType'], - skip: (page - 1) * pageSize, - take: pageSize, + // Create the new community entity + const community = this.communityRepository.create({ + name: name, + description: description, + region: region, // Associate with the found region }); - if (!children || children.length === 0 || !includeSubSpaces) { - return children - .filter((child) => child.spaceType.type !== SpaceType.COMMUNITY) // Filter remaining community type - .map((child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - })); + // Save the community to the database + try { + await this.communityRepository.save(community); + return new SuccessResponseDto({ + statusCode: HttpStatus.CREATED, + success: true, + data: community, + message: 'Community created successfully', + }); + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + async getCommunityById(id: string): Promise { + const community = await this.communityRepository.findOneBy({ uuid: id }); + + // If the community is not found, throw a 404 NotFoundException + if (!community) { + throw new HttpException( + `Community with ID ${id} not found.`, + HttpStatus.NOT_FOUND, + ); } - const childHierarchies = await Promise.all( - children - .filter((child) => child.spaceType.type !== SpaceType.COMMUNITY) // Filter remaining community type - .map(async (child) => ({ - uuid: child.uuid, - name: child.spaceName, - type: child.spaceType.type, - children: await this.buildHierarchy(child, true, 1, pageSize), - })), - ); + // Return a success response + return new SuccessResponseDto({ + data: community, + message: 'Community fetched successfully', + }); + } - return childHierarchies; + async getCommunities( + pageable: Partial, + ): Promise { + pageable.modelName = 'community'; + + const customModel = TypeORMCustomModel(this.communityRepository); + + const { baseResponseDto, paginationResponseDto } = + await customModel.findAll(pageable); + + return new PageResponse( + baseResponseDto, + paginationResponseDto, + ); + } + + async updateCommunity( + id: string, + updateCommunityDto: UpdateCommunityNameDto, + ): Promise { + const community = await this.communityRepository.findOne({ + where: { uuid: id }, + }); + + // If the community doesn't exist, throw a 404 error + if (!community) { + throw new HttpException( + `Community with ID ${id} not found`, + HttpStatus.NOT_FOUND, + ); + } + + try { + const { name } = updateCommunityDto; + + // Find the community by its UUID + + // Update the community name + community.name = name; + + // Save the updated community back to the database + const updatedCommunity = await this.communityRepository.save(community); + + // Return a SuccessResponseDto with the updated community data + return new SuccessResponseDto({ + message: 'Success update Community', + data: updatedCommunity, + }); + } catch (err) { + // Catch and handle any errors + if (err instanceof HttpException) { + throw err; // If it's an HttpException, rethrow it + } else { + // Throw a generic 404 error if anything else goes wrong + throw new HttpException('Community not found', HttpStatus.NOT_FOUND); + } + } + } + + async deleteCommunity(id: string): Promise { + const community = await this.communityRepository.findOne({ + where: { uuid: id }, + }); + + // If the community is not found, throw an error + if (!community) { + throw new HttpException( + `Community with ID ${id} not found`, + HttpStatus.NOT_FOUND, + ); + } + try { + // Find the community by its uuid + + // Remove the community from the database + await this.communityRepository.remove(community); + + // Return success response + return new SuccessResponseDto({ + message: `Community with ID ${id} successfully deleted`, + data: null, + }); + } catch (err) { + // Catch and handle any errors + if (err instanceof HttpException) { + throw err; // If it's an HttpException, rethrow it + } else { + throw new HttpException( + 'An error occurred while deleting the community', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } async getCommunitiesByUserId( @@ -188,7 +178,6 @@ export class CommunityService { relations: ['space', 'space.spaceType'], where: { user: { uuid: userUuid }, - space: { spaceType: { type: SpaceType.COMMUNITY } }, }, }); @@ -200,7 +189,7 @@ export class CommunityService { } const spaces = communities.map((community) => ({ uuid: community.space.uuid, - name: community.space.spaceName, + name: community.space.name, type: community.space.spaceType.type, })); @@ -221,7 +210,7 @@ export class CommunityService { space: { uuid: addUserCommunityDto.communityUuid }, }); } catch (err) { - if (err.code === CommonErrorCodes.DUPLICATE_ENTITY) { + if (err.code === '23505') { throw new HttpException( 'User already belongs to this community', HttpStatus.BAD_REQUEST, @@ -233,46 +222,4 @@ export class CommunityService { ); } } - async renameCommunityByUuid( - communityUuid: string, - updateCommunityDto: UpdateCommunityNameDto, - ): Promise { - try { - const community = await this.spaceRepository.findOneOrFail({ - where: { uuid: communityUuid }, - relations: ['spaceType'], - }); - - if ( - !community || - !community.spaceType || - community.spaceType.type !== SpaceType.COMMUNITY - ) { - throw new BadRequestException('Invalid community UUID'); - } - - await this.spaceRepository.update( - { uuid: communityUuid }, - { spaceName: updateCommunityDto.communityName }, - ); - - // Fetch the updated community - const updatedCommunity = await this.spaceRepository.findOneOrFail({ - where: { uuid: communityUuid }, - relations: ['spaceType'], - }); - - return { - uuid: updatedCommunity.uuid, - name: updatedCommunity.spaceName, - type: updatedCommunity.spaceType.type, - }; - } catch (err) { - if (err instanceof BadRequestException) { - throw err; // Re-throw BadRequestException - } else { - throw new HttpException('Community not found', HttpStatus.NOT_FOUND); - } - } - } }