diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 784cb78..204bf33 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -6,9 +6,10 @@ import { AuthModule } from './auth/auth.module' import { GeolocationsModule } from './geolocations/geolocations.module' import { ChallengesModule } from './challenges/challenges.module' import { ChallengeActionsModule } from './challenge-actions/challenge-actions.module' +import { TrainsModule } from './trains/trains.module'; @Module({ - imports: [PrismaModule, UsersModule, AuthModule, GeolocationsModule, ChallengesModule, ChallengeActionsModule], + imports: [PrismaModule, UsersModule, AuthModule, GeolocationsModule, ChallengesModule, ChallengeActionsModule, TrainsModule], providers: [PrismaService], }) export class AppModule {} diff --git a/server/src/trains/dto/create-train.dto.ts b/server/src/trains/dto/create-train.dto.ts new file mode 100644 index 0000000..51362b0 --- /dev/null +++ b/server/src/trains/dto/create-train.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty } from "@nestjs/swagger" +import { JsonValue } from "@prisma/client/runtime/library" +import { Type } from "class-transformer" +import { IsDate, IsInt, IsJSON, IsNumber, IsString } from "class-validator" + +export class CreateTrainDto { + @IsString() + @ApiProperty({ description: "Identifiant du train, donné par l'identifiant de partage Interrail" }) + id: string + + @IsInt() + @Type(() => Number) + @ApiProperty({ description: "Identifiant de l'utilisateur⋅rice effectuant le trajet" }) + userId: number + + @IsNumber() + @Type(() => Number) + @ApiProperty({ description: "Distance en mètres du trajet, calculé sur https://signal.eu.org/osm/" }) + distance: number + + @IsString() + @ApiProperty({ description: "Nom de la gare de départ" }) + from: string + + @IsString() + @ApiProperty({ description: "Nom de la gare d'arrivée" }) + to: string + + @IsDate() + @Type(() => Date) + @ApiProperty({ description: "Date et heure de départ du train" }) + departureTime: Date + + @IsDate() + @Type(() => Date) + @ApiProperty({ description: "Date et heure d'arrivée du train" }) + arrivalTime: Date + + @IsJSON() + @ApiProperty({ description: "Informations JSON supplémentaires transmises par Interrail" }) + infoJson: JsonValue + + @IsString() + @ApiProperty({ description: "Géométrie de la course, obtenue par https://signal.eu.org/osm/" }) + geometry: string +} diff --git a/server/src/trains/dto/update-train.dto.ts b/server/src/trains/dto/update-train.dto.ts new file mode 100644 index 0000000..0626856 --- /dev/null +++ b/server/src/trains/dto/update-train.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateTrainDto } from './create-train.dto'; + +export class UpdateTrainDto extends PartialType(CreateTrainDto) {} diff --git a/server/src/trains/entities/train.entity.ts b/server/src/trains/entities/train.entity.ts new file mode 100644 index 0000000..7f87553 --- /dev/null +++ b/server/src/trains/entities/train.entity.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from "@nestjs/swagger" +import { TrainTrip } from "@prisma/client" +import { JsonValue } from "@prisma/client/runtime/library" + +export class TrainEntity implements TrainTrip { + constructor (partial: Partial) { + Object.assign(this, partial) + } + + @ApiProperty({ description: "Identifiant du train, donné par l'identifiant de partage Interrail" }) + id: string + + @ApiProperty({ description: "Identifiant de l'utilisateur⋅rice effectuant le trajet" }) + userId: number + + @ApiProperty({ description: "Distance en mètres du trajet, calculé sur https://signal.eu.org/osm/" }) + distance: number + + @ApiProperty({ description: "Nom de la gare de départ" }) + from: string + + @ApiProperty({ description: "Nom de la gare d'arrivée" }) + to: string + + @ApiProperty({ description: "Date et heure de départ du train" }) + departureTime: Date + + @ApiProperty({ description: "Date et heure d'arrivée du train" }) + arrivalTime: Date + + @ApiProperty({ description: "Informations JSON supplémentaires transmises par Interrail" }) + infoJson: JsonValue + + @ApiProperty({ description: "Géométrie de la course, obtenue par https://signal.eu.org/osm/" }) + geometry: string +} diff --git a/server/src/trains/trains.controller.spec.ts b/server/src/trains/trains.controller.spec.ts new file mode 100644 index 0000000..6a46cfe --- /dev/null +++ b/server/src/trains/trains.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { TrainsController } from './trains.controller' +import { TrainsService } from './trains.service' + +describe('TrainsController', () => { + let controller: TrainsController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [TrainsController], + providers: [TrainsService], + }).compile() + + controller = module.get(TrainsController) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) +}) diff --git a/server/src/trains/trains.controller.ts b/server/src/trains/trains.controller.ts new file mode 100644 index 0000000..0c5a91a --- /dev/null +++ b/server/src/trains/trains.controller.ts @@ -0,0 +1,75 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, HttpCode, UseGuards, Query, ParseIntPipe, NotFoundException } from '@nestjs/common' +import { TrainsService } from './trains.service' +import { CreateTrainDto } from './dto/create-train.dto' +import { UpdateTrainDto } from './dto/update-train.dto' +import { TrainEntity } from './entities/train.entity' +import { JwtAuthGuard } from 'src/auth/jwt-auth.guard' +import { ApiBearerAuth, ApiCreatedResponse, ApiForbiddenResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger' +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('trains') +export class TrainsController { + constructor(private readonly trainsService: TrainsService) {} + + @Post() + @HttpCode(201) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiCreatedResponse({ type: TrainEntity, description: "Objet créé avec succès" }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + async create(@Body() createTrainDto: CreateTrainDto): Promise { + const train = await this.trainsService.create(createTrainDto) + return new TrainEntity(train) + } + + @Get() + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponsePaginated(TrainEntity) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + async findAll(@Query() queryPagination?: QueryPaginationDto): Promise> { + const [trains, total] = await this.trainsService.findAll(queryPagination) + return paginateOutput(trains.map(train => new TrainEntity(train)), total, queryPagination) + } + + @Get(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: TrainEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async findOne(@Param('id') id: string): Promise { + const train = await this.trainsService.findOne(id) + if (!train) + throw new NotFoundException(`Trajet en train inexistant avec l'identifiant ${id}`) + return new TrainEntity(train) + } + + @Patch(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: TrainEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async update(@Param('id') id: string, @Body() updateChallengeDto: UpdateTrainDto) { + return await this.trainsService.update(id, updateChallengeDto) + } + + @Delete(':id') + @HttpCode(204) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: TrainEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async remove(@Param('id') id: string) { + await this.trainsService.remove(id) + } +} diff --git a/server/src/trains/trains.module.ts b/server/src/trains/trains.module.ts new file mode 100644 index 0000000..5a32b6c --- /dev/null +++ b/server/src/trains/trains.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { TrainsService } from './trains.service' +import { TrainsController } from './trains.controller' +import { PrismaModule } from 'src/prisma/prisma.module' + +@Module({ + controllers: [TrainsController], + providers: [TrainsService], + imports: [PrismaModule], +}) +export class TrainsModule {} diff --git a/server/src/trains/trains.service.spec.ts b/server/src/trains/trains.service.spec.ts new file mode 100644 index 0000000..0ed4398 --- /dev/null +++ b/server/src/trains/trains.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { TrainsService } from './trains.service' + +describe('TrainsService', () => { + let service: TrainsService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [TrainsService], + }).compile() + + service = module.get(TrainsService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/server/src/trains/trains.service.ts b/server/src/trains/trains.service.ts new file mode 100644 index 0000000..b872399 --- /dev/null +++ b/server/src/trains/trains.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common' +import { CreateTrainDto } from './dto/create-train.dto' +import { UpdateTrainDto } from './dto/update-train.dto' +import { PrismaService } from 'src/prisma/prisma.service' +import { TrainTrip } from '@prisma/client' +import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' +import { paginate } from 'src/common/utils/pagination.utils' + +@Injectable() +export class TrainsService { + constructor(private prisma: PrismaService) { } + + async create(createTrainDto: CreateTrainDto): Promise { + return await this.prisma.trainTrip.create({ data: createTrainDto }) + } + + async findAll(queryPagination?: QueryPaginationDto): Promise<[TrainTrip[], number]> { + return [ + await this.prisma.trainTrip.findMany({ + ...paginate(queryPagination), + }), + await this.prisma.challenge.count(), + ] + } + + async findOne(id: string): Promise { + return await this.prisma.trainTrip.findUnique({ + where: { id }, + }) + } + + async update(id: string, updateTrainDto: UpdateTrainDto): Promise { + return await this.prisma.trainTrip.update({ + where: { id }, + data: updateTrainDto, + }) + } + + async remove(id: string): Promise { + return await this.prisma.trainTrip.delete({ + where: { id }, + }) + } +}