Ajout de la pagination sur l'API
This commit is contained in:
parent
86427bb41b
commit
138ff1df65
11
server/package-lock.json
generated
11
server/package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"@nestjs/swagger": "^8.1.0",
|
"@nestjs/swagger": "^8.1.0",
|
||||||
"@prisma/client": "^6.0.1",
|
"@prisma/client": "^6.0.1",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
@ -3674,12 +3675,10 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/class-transformer": {
|
"node_modules/class-transformer": {
|
||||||
"version": "0.4.0",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||||
"integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==",
|
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true,
|
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/class-validator": {
|
"node_modules/class-validator": {
|
||||||
"version": "0.14.1",
|
"version": "0.14.1",
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"@nestjs/swagger": "^8.1.0",
|
"@nestjs/swagger": "^8.1.0",
|
||||||
"@prisma/client": "^6.0.1",
|
"@prisma/client": "^6.0.1",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
42
server/src/common/dto/pagination-output.dto.ts
Normal file
42
server/src/common/dto/pagination-output.dto.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger"
|
||||||
|
import { IsNumber, IsOptional } from "class-validator"
|
||||||
|
|
||||||
|
export const DEFAULT_PAGE_NUMBER = 1
|
||||||
|
export const DEFAULT_PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export class MetaPaginateOutputDto {
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty()
|
||||||
|
total: number
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty()
|
||||||
|
lastPage: number
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({ default: DEFAULT_PAGE_NUMBER })
|
||||||
|
currentPage: number = DEFAULT_PAGE_NUMBER
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({ default: DEFAULT_PAGE_SIZE })
|
||||||
|
totalPerPage: number = DEFAULT_PAGE_SIZE
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({ required: false, nullable: true, default: null })
|
||||||
|
prevPage?: number | null
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({ required: false, nullable: true, default: null })
|
||||||
|
nextPage?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaginateOutputDto<T> {
|
||||||
|
@ApiProperty({ isArray: true })
|
||||||
|
data: T[]
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
meta: MetaPaginateOutputDto
|
||||||
|
}
|
||||||
|
|
17
server/src/common/dto/pagination-query.dto.ts
Normal file
17
server/src/common/dto/pagination-query.dto.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger'
|
||||||
|
import { Type } from 'class-transformer'
|
||||||
|
import { IsNumber, IsOptional } from 'class-validator'
|
||||||
|
|
||||||
|
export class QueryPaginationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Type(() => Number)
|
||||||
|
@ApiProperty({default: 1, required: false, description: "Numéro de page à charger"})
|
||||||
|
page?: number = 1
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Type(() => Number)
|
||||||
|
@ApiProperty({default: 20, required: false, description: "Nombre d'éléments à charger par page"})
|
||||||
|
size?: number = 20
|
||||||
|
}
|
86
server/src/common/utils/pagination.utils.ts
Normal file
86
server/src/common/utils/pagination.utils.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { applyDecorators, NotFoundException, Type } from '@nestjs/common'
|
||||||
|
import { QueryPaginationDto } from '../dto/pagination-query.dto'
|
||||||
|
import { ApiExtraModels, ApiOkResponse, ApiResponseNoStatusOptions, getSchemaPath } from '@nestjs/swagger'
|
||||||
|
import { DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE, PaginateOutputDto } from '../dto/pagination-output.dto'
|
||||||
|
|
||||||
|
export interface PrismaPaginationParams {
|
||||||
|
skip: number
|
||||||
|
take: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paginate = (
|
||||||
|
query: QueryPaginationDto,
|
||||||
|
): PrismaPaginationParams => {
|
||||||
|
const size = query.size || DEFAULT_PAGE_SIZE
|
||||||
|
const page = query.page || DEFAULT_PAGE_NUMBER
|
||||||
|
return {
|
||||||
|
skip: size * (page - 1),
|
||||||
|
take: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paginateOutput = <T>(
|
||||||
|
data: T[],
|
||||||
|
total: number,
|
||||||
|
query: QueryPaginationDto,
|
||||||
|
): PaginateOutputDto<T> => {
|
||||||
|
const page = query.page || DEFAULT_PAGE_NUMBER
|
||||||
|
const size = query.size || DEFAULT_PAGE_SIZE
|
||||||
|
|
||||||
|
const lastPage = Math.ceil(total / size)
|
||||||
|
|
||||||
|
// if data is empty, return empty array
|
||||||
|
if (!data.length) {
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
lastPage,
|
||||||
|
currentPage: page,
|
||||||
|
totalPerPage: size,
|
||||||
|
prevPage: null,
|
||||||
|
nextPage: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if page is greater than last page, throw an error
|
||||||
|
if (page > lastPage) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Page ${page} not found. Last page is ${lastPage}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
lastPage,
|
||||||
|
currentPage: page,
|
||||||
|
totalPerPage: size,
|
||||||
|
prevPage: page > 1 ? page - 1 : null,
|
||||||
|
nextPage: page < lastPage ? page + 1 : null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiOkResponsePaginated = <DataDto extends Type<unknown>>(dataDto: DataDto, options?: ApiResponseNoStatusOptions) =>
|
||||||
|
applyDecorators(
|
||||||
|
ApiExtraModels(PaginateOutputDto, dataDto),
|
||||||
|
ApiOkResponse({
|
||||||
|
...options,
|
||||||
|
schema: {
|
||||||
|
allOf: [
|
||||||
|
{ $ref: getSchemaPath(PaginateOutputDto) },
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
type: 'array',
|
||||||
|
items: { $ref: getSchemaPath(dataDto) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
@ -1,9 +1,12 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, UseGuards, HttpCode, Req, NotFoundException } from '@nestjs/common'
|
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, UseGuards, HttpCode, Req, NotFoundException, Query } from '@nestjs/common'
|
||||||
import { GeolocationsService } from './geolocations.service'
|
import { GeolocationsService } from './geolocations.service'
|
||||||
import { CreateGeolocationDto } from './dto/create-geolocation.dto'
|
import { CreateGeolocationDto } from './dto/create-geolocation.dto'
|
||||||
import { AuthenticatedRequest, JwtAuthGuard } from 'src/auth/jwt-auth.guard'
|
import { AuthenticatedRequest, JwtAuthGuard } from 'src/auth/jwt-auth.guard'
|
||||||
import { ApiBearerAuth, ApiCreatedResponse, ApiForbiddenResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger'
|
import { ApiBearerAuth, ApiCreatedResponse, ApiForbiddenResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger'
|
||||||
import { GeolocationEntity } from './entities/geolocation.entity'
|
import { GeolocationEntity } from './entities/geolocation.entity'
|
||||||
|
import { ApiOkResponsePaginated, paginateOutput } from 'src/common/utils/pagination.utils'
|
||||||
|
import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto'
|
||||||
|
import { PaginateOutputDto } from 'src/common/dto/pagination-output.dto'
|
||||||
|
|
||||||
@Controller('geolocations')
|
@Controller('geolocations')
|
||||||
export class GeolocationsController {
|
export class GeolocationsController {
|
||||||
@ -26,13 +29,13 @@ export class GeolocationsController {
|
|||||||
@Get()
|
@Get()
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ type: GeolocationEntity, isArray: true })
|
@ApiOkResponsePaginated(GeolocationEntity)
|
||||||
@ApiUnauthorizedResponse({ description: "Non authentifié⋅e" })
|
@ApiUnauthorizedResponse({ description: "Non authentifié⋅e" })
|
||||||
@ApiForbiddenResponse({ description: "Permission refusée" })
|
@ApiForbiddenResponse({ description: "Permission refusée" })
|
||||||
@ApiNotFoundResponse({ description: "Objet non trouvé" })
|
@ApiNotFoundResponse({ description: "Objet non trouvé" })
|
||||||
async findAll(): Promise<GeolocationEntity[]> {
|
async findAll(@Query() queryPagination?: QueryPaginationDto): Promise<PaginateOutputDto<GeolocationEntity>> {
|
||||||
const geolocations = await this.geolocationsService.findAll()
|
const [geolocations, total] = await this.geolocationsService.findAll(queryPagination)
|
||||||
return geolocations.map(geolocation => new GeolocationEntity(geolocation))
|
return paginateOutput<GeolocationEntity>(geolocations.map(geolocation => new GeolocationEntity(geolocation)), total, queryPagination)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'
|
|||||||
import { CreateGeolocationDto } from './dto/create-geolocation.dto'
|
import { CreateGeolocationDto } from './dto/create-geolocation.dto'
|
||||||
import { PrismaService } from 'src/prisma/prisma.service'
|
import { PrismaService } from 'src/prisma/prisma.service'
|
||||||
import { Geolocation, User } from '@prisma/client'
|
import { Geolocation, User } from '@prisma/client'
|
||||||
|
import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto'
|
||||||
|
import { paginate } from 'src/common/utils/pagination.utils'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GeolocationsService {
|
export class GeolocationsService {
|
||||||
@ -12,8 +14,13 @@ export class GeolocationsService {
|
|||||||
return await this.prisma.geolocation.create({ data: data })
|
return await this.prisma.geolocation.create({ data: data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(): Promise<Geolocation[]> {
|
async findAll(queryPagination?: QueryPaginationDto): Promise<[Geolocation[], number]> {
|
||||||
return await this.prisma.geolocation.findMany()
|
return [
|
||||||
|
await this.prisma.geolocation.findMany({
|
||||||
|
...paginate(queryPagination),
|
||||||
|
}),
|
||||||
|
await this.prisma.geolocation.count(),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number): Promise<Geolocation> {
|
async findOne(id: number): Promise<Geolocation> {
|
||||||
|
@ -6,7 +6,7 @@ import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule)
|
const app = await NestFactory.create(AppModule)
|
||||||
|
|
||||||
app.useGlobalPipes(new ValidationPipe())
|
app.useGlobalPipes(new ValidationPipe({ transform: true }))
|
||||||
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))
|
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { Body, Controller, Get, HttpCode, NotFoundException, Param, ParseIntPipe, Patch, Req, UseGuards } from '@nestjs/common'
|
import { Body, Controller, Get, HttpCode, NotFoundException, Param, ParseIntPipe, Patch, Query, Req, UseGuards } from '@nestjs/common'
|
||||||
import { UsersService } from './users.service'
|
import { UsersService } from './users.service'
|
||||||
import { ApiBadRequestResponse, ApiBearerAuth, ApiForbiddenResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger'
|
import { ApiBadRequestResponse, ApiBearerAuth, ApiForbiddenResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger'
|
||||||
import { UserEntity } from './entities/user.entity'
|
import { UserEntity } from './entities/user.entity'
|
||||||
import { AuthenticatedRequest, JwtAuthGuard } from 'src/auth/jwt-auth.guard'
|
import { AuthenticatedRequest, JwtAuthGuard } from 'src/auth/jwt-auth.guard'
|
||||||
import { UpdatePasswordDto } from './dto/user_password.dto'
|
import { UpdatePasswordDto } from './dto/user_password.dto'
|
||||||
|
import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto'
|
||||||
|
import { ApiOkResponsePaginated, paginateOutput } from 'src/common/utils/pagination.utils'
|
||||||
|
import { PaginateOutputDto } from 'src/common/dto/pagination-output.dto'
|
||||||
|
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
@ -12,12 +15,12 @@ export class UsersController {
|
|||||||
@Get()
|
@Get()
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ type: UserEntity, isArray: true })
|
@ApiOkResponsePaginated(UserEntity)
|
||||||
@ApiUnauthorizedResponse({ description: "Non authentifié⋅e" })
|
@ApiUnauthorizedResponse({ description: "Non authentifié⋅e" })
|
||||||
@ApiForbiddenResponse({ description: "Permission refusée" })
|
@ApiForbiddenResponse({ description: "Permission refusée" })
|
||||||
async findAll() {
|
async findAll(@Query() queryPagination?: QueryPaginationDto): Promise<PaginateOutputDto<UserEntity>> {
|
||||||
const users = await this.usersService.findAll()
|
const [users, total] = await this.usersService.findAll(queryPagination)
|
||||||
return users.map(user => new UserEntity(user))
|
return paginateOutput<UserEntity>(users.map(user => new UserEntity(user)), total, queryPagination)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@ -3,20 +3,25 @@ import { User } from '@prisma/client'
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service'
|
import { PrismaService } from 'src/prisma/prisma.service'
|
||||||
import * as bcrypt from 'bcrypt'
|
import * as bcrypt from 'bcrypt'
|
||||||
import { UpdatePasswordDto } from './dto/user_password.dto'
|
import { UpdatePasswordDto } from './dto/user_password.dto'
|
||||||
|
import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto'
|
||||||
|
import { paginate } from 'src/common/utils/pagination.utils'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
async findAll() {
|
async findAll(queryPagination?: QueryPaginationDto): Promise<[User[], number]> {
|
||||||
return await this.prisma.user.findMany()
|
return [
|
||||||
|
await this.prisma.user.findMany({ ...paginate(queryPagination) }),
|
||||||
|
await this.prisma.user.count()
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number) {
|
async findOne(id: number): Promise<User> {
|
||||||
return await this.prisma.user.findUnique({ where: { id } })
|
return await this.prisma.user.findUnique({ where: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePassword(user: User, { password }: UpdatePasswordDto) {
|
async updatePassword(user: User, { password }: UpdatePasswordDto): Promise<void> {
|
||||||
const hashedPassword = await bcrypt.hash(password, 10)
|
const hashedPassword = await bcrypt.hash(password, 10)
|
||||||
await this.prisma.user.update({
|
await this.prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
|
Loading…
Reference in New Issue
Block a user