community controller

This commit is contained in:
hannathkadher
2024-10-15 11:01:56 +04:00
parent a157444897
commit 2292c01220
23 changed files with 822 additions and 259 deletions

View File

@ -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';
};
};
}

View File

@ -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,

View File

@ -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<BaseResponseDto, 'message'>) {
return { data, statusCode, success, message, error };
}
}

View File

@ -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;
}

View File

@ -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<T> implements BaseResponseDto, PageResponseDto {
code?: number;
message: string;
data: Array<T>;
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;
}
}

View File

@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { BaseResponseDto } from './base.response.dto';
export class SuccessResponseDto<Type> 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;
}
}

View File

@ -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<TypeORMCustomModelFindAllQuery>,
): 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<any>) {
return Object.assign(repository, {
findAll: async function (
query: Partial<TypeORMCustomModelFindAllQuery>,
): Promise<TypeORMCustomModelFindAllResponse> {
// 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}`;
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from './community.dto';

View File

@ -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<CommunityDto> {
@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[];
}

View File

@ -0,0 +1 @@
export * from './community.entity';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { CommunityEntity } from '../entities';
@Injectable()
export class CommunityRepository extends Repository<CommunityEntity> {
constructor(private dataSource: DataSource) {
super(CommunityEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1 @@
export * from './community.repository';

View File

@ -0,0 +1,2 @@
export type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
export type WithRequired<T, K extends keyof T> = Omit<T, K> & Required<T>;

View File

@ -0,0 +1,4 @@
export function parseToDate(value: unknown): Date {
const valueInNumber = Number(value);
return new Date(valueInNumber);
}

View File

@ -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<boolean> here as well, if you want to make async validation
},
},
},
validationOptions,
);
}
function IsPageParam(value: any): boolean {
return !isNaN(Number(value)) && value > 0;
}

View File

@ -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<boolean> here as well, if you want to make async validation
},
},
},
validationOptions,
);
}
function IsSizeParam(value: any): boolean {
return !isNaN(Number(value)) && value > -1;
}

View File

@ -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<boolean> 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;
}