Skip to content

Commit

Permalink
feat(users): added filtration and sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
Shchepotin committed Oct 18, 2023
1 parent 681bb29 commit 6f7adb1
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 23 deletions.
3 changes: 2 additions & 1 deletion src/roles/entities/role.entity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { Allow } from 'class-validator';
import { Allow, IsNumber } from 'class-validator';
import { EntityHelper } from 'src/utils/entity-helper';

@Entity()
export class Role extends EntityHelper {
@ApiProperty({ example: 1 })
@PrimaryColumn()
@IsNumber()
id: number;

@Allow()
Expand Down
65 changes: 65 additions & 0 deletions src/users/dto/query-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ApiProperty } from '@nestjs/swagger';
import { Role } from '../../roles/entities/role.entity';
import {
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { Transform, Type, plainToInstance } from 'class-transformer';
import { User } from '../entities/user.entity';

export class FilterUserDto {
@ApiProperty({ type: Role })
@IsOptional()
@ValidateNested({ each: true })
@Type(() => Role)
roles?: Role[] | null;
}

export class SortUserDto {
@ApiProperty()
@IsString()
orderBy: keyof User;

@ApiProperty()
@IsString()
order: string;
}

export class QueryUserDto {
@ApiProperty({
required: false,
})
@Transform(({ value }) => (value ? Number(value) : 1))
@IsNumber()
@IsOptional()
page: number;

@ApiProperty({
required: false,
})
@Transform(({ value }) => (value ? Number(value) : 10))
@IsNumber()
@IsOptional()
limit: number;

@ApiProperty({ type: String, required: false })
@IsOptional()
@Transform(({ value }) =>
value ? plainToInstance(FilterUserDto, JSON.parse(value)) : undefined,
)
@ValidateNested()
@Type(() => FilterUserDto)
filters?: FilterUserDto | null;

@ApiProperty({ type: String, required: false })
@IsOptional()
@Transform(({ value }) => {
console.log(JSON.parse(value));
return value ? plainToInstance(SortUserDto, JSON.parse(value)) : undefined;
})
@ValidateNested({ each: true })
@Type(() => SortUserDto)
sort?: SortUserDto[] | null;
}
16 changes: 10 additions & 6 deletions src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {
Delete,
UseGuards,
Query,
DefaultValuePipe,
ParseIntPipe,
HttpStatus,
HttpCode,
SerializeOptions,
Expand All @@ -26,6 +24,7 @@ import { infinityPagination } from 'src/utils/infinity-pagination';
import { User } from './entities/user.entity';
import { InfinityPaginationResultType } from '../utils/types/infinity-pagination-result.type';
import { NullableType } from '../utils/types/nullable.type';
import { QueryUserDto } from './dto/query-user.dto';

@ApiBearerAuth()
@Roles(RoleEnum.admin)
Expand Down Expand Up @@ -53,17 +52,22 @@ export class UsersController {
@Get()
@HttpCode(HttpStatus.OK)
async findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query() query: QueryUserDto,
): Promise<InfinityPaginationResultType<User>> {
const page = query?.page ?? 1;
let limit = query?.limit ?? 10;
if (limit > 50) {
limit = 50;
}

return infinityPagination(
await this.usersService.findManyWithPagination({
page,
limit,
filterOptions: query?.filters,
sortOptions: query?.sort,
paginationOptions: {
page,
limit,
},
}),
{ page, limit },
);
Expand Down
30 changes: 26 additions & 4 deletions src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityCondition } from 'src/utils/types/entity-condition.type';
import { IPaginationOptions } from 'src/utils/types/pagination-options';
import { DeepPartial, Repository } from 'typeorm';
import { DeepPartial, FindOptionsWhere, Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';
import { NullableType } from '../utils/types/nullable.type';
import { FilterUserDto, SortUserDto } from './dto/query-user.dto';

@Injectable()
export class UsersService {
Expand All @@ -20,12 +21,33 @@ export class UsersService {
);
}

findManyWithPagination(
paginationOptions: IPaginationOptions,
): Promise<User[]> {
findManyWithPagination({
filterOptions,
sortOptions,
paginationOptions,
}: {
filterOptions?: FilterUserDto | null;
sortOptions?: SortUserDto[] | null;
paginationOptions: IPaginationOptions;
}): Promise<User[]> {
const where: FindOptionsWhere<User> = {};
if (filterOptions?.roles?.length) {
where.role = filterOptions.roles.map((role) => ({
id: role.id,
}));
}

return this.usersRepository.find({
skip: (paginationOptions.page - 1) * paginationOptions.limit,
take: paginationOptions.limit,
where: where,
order: sortOptions?.reduce(
(accumulator, sort) => ({
...accumulator,
[sort.orderBy]: sort.order,
}),
{},
),
});
}

Expand Down
30 changes: 18 additions & 12 deletions src/utils/validation-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,32 @@ import {
ValidationPipeOptions,
} from '@nestjs/common';

function generateErrors(errors: ValidationError[]) {
return errors.reduce(
(accumulator, currentValue) => ({
...accumulator,
[currentValue.property]:
(currentValue.children?.length ?? 0) > 0
? generateErrors(currentValue.children ?? [])
: Object.values(currentValue.constraints ?? {}).join(', '),
}),
{},
);
}

const validationOptions: ValidationPipeOptions = {
transform: true,
whitelist: true,
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
exceptionFactory: (errors: ValidationError[]) =>
new HttpException(
exceptionFactory: (errors: ValidationError[]) => {
return new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: errors.reduce(
(accumulator, currentValue) => ({
...accumulator,
[currentValue.property]: Object.values(
currentValue.constraints ?? {},
).join(', '),
}),
{},
),
errors: generateErrors(errors),
},
HttpStatus.UNPROCESSABLE_ENTITY,
),
);
},
};

export default validationOptions;

0 comments on commit 6f7adb1

Please sign in to comment.