diff --git a/apps/backend/src/reservations/dto/create-reservation.dto.ts b/apps/backend/src/reservations/dto/create-reservation.dto.ts new file mode 100644 index 0000000..6e3526b --- /dev/null +++ b/apps/backend/src/reservations/dto/create-reservation.dto.ts @@ -0,0 +1,5 @@ +import { OmitType } from '@nestjs/swagger'; + +import { Reservation } from '../entities/reservation.entity'; + +export class CreateReservationDto extends OmitType(Reservation, ['id', 'userId']) {} diff --git a/apps/backend/src/reservations/dto/simple-reservation.dto.ts b/apps/backend/src/reservations/dto/simple-reservation.dto.ts new file mode 100644 index 0000000..7ddcf96 --- /dev/null +++ b/apps/backend/src/reservations/dto/simple-reservation.dto.ts @@ -0,0 +1,5 @@ +import { OmitType } from '@nestjs/swagger'; + +import { Reservation } from '../entities/reservation.entity'; + +export class SimpleReservationDto extends OmitType(Reservation, ['id', 'userId']) {} diff --git a/apps/backend/src/reservations/dto/update-reservation.dto.ts b/apps/backend/src/reservations/dto/update-reservation.dto.ts new file mode 100644 index 0000000..f16bf66 --- /dev/null +++ b/apps/backend/src/reservations/dto/update-reservation.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/mapped-types'; + +import { CreateReservationDto } from './create-reservation.dto'; + +export class UpdateReservationDto extends PartialType(CreateReservationDto) {} diff --git a/apps/backend/src/reservations/entities/reservation.entity.ts b/apps/backend/src/reservations/entities/reservation.entity.ts new file mode 100644 index 0000000..42ff503 --- /dev/null +++ b/apps/backend/src/reservations/entities/reservation.entity.ts @@ -0,0 +1,29 @@ +import { ReservationStatus } from '@prisma/client'; +import { IsDate, IsEnum, IsNotEmpty, IsNumber } from 'class-validator'; + +export class Reservation { + @IsNotEmpty() + @IsNumber() + id: number; + + @IsNumber() + userId: number; + + @IsNumber() + bandId: number; + + @IsNotEmpty() + @IsDate() + startTime: Date; + + @IsNotEmpty() + @IsDate() + endTime: Date; + + @IsNumber() + gateKeeperId: number; + + @IsNotEmpty() + @IsEnum(ReservationStatus) + status: ReservationStatus; +} diff --git a/apps/backend/src/reservations/reservations.controller.ts b/apps/backend/src/reservations/reservations.controller.ts new file mode 100644 index 0000000..2d663a3 --- /dev/null +++ b/apps/backend/src/reservations/reservations.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Query } from '@nestjs/common'; + +import { CreateReservationDto } from './dto/create-reservation.dto'; +import { UpdateReservationDto } from './dto/update-reservation.dto'; +import { ReservationsService } from './reservations.service'; + +@Controller('reservations') +export class ReservationsController { + constructor(private readonly reservationsService: ReservationsService) {} + + @Post() + create(@Body() createReservationDto: CreateReservationDto) { + return this.reservationsService.create(createReservationDto); + } + + @Get() + async findAll(@Query('page', ParseIntPipe) page: number, @Query('page_size', ParseIntPipe) pageSize: number) { + return this.reservationsService.findAll(page, pageSize); + } + + @Get(':id') + async findOne(@Param('id', ParseIntPipe) id: number) { + return this.reservationsService.findOne(id); + } + + @Patch(':id') + update(@Param('id', ParseIntPipe) id: number, @Body() updateReservationDto: UpdateReservationDto) { + return this.reservationsService.update(id, updateReservationDto); + } + + @Delete(':id') + remove(@Param('id', ParseIntPipe) id: number) { + return this.reservationsService.remove(id); + } +} diff --git a/apps/backend/src/reservations/reservations.module.ts b/apps/backend/src/reservations/reservations.module.ts new file mode 100644 index 0000000..49f1544 --- /dev/null +++ b/apps/backend/src/reservations/reservations.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { ReservationsController } from './reservations.controller'; +import { ReservationsService } from './reservations.service'; + +@Module({ + controllers: [ReservationsController], + providers: [ReservationsService], +}) +export class ReservationsModule {} diff --git a/apps/backend/src/reservations/reservations.service.ts b/apps/backend/src/reservations/reservations.service.ts new file mode 100644 index 0000000..00f5f9b --- /dev/null +++ b/apps/backend/src/reservations/reservations.service.ts @@ -0,0 +1,102 @@ +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from 'nestjs-prisma'; + +import { PaginationDto } from '../dto/pagination.dto'; +import { CreateReservationDto } from './dto/create-reservation.dto'; +import { UpdateReservationDto } from './dto/update-reservation.dto'; +import { Reservation } from './entities/reservation.entity'; + +@Injectable() +export class ReservationsService { + constructor(private readonly prisma: PrismaService) {} + + create(createReservationDto: CreateReservationDto) { + return this.prisma.reservation.create({ + data: { + ...createReservationDto, + }, + }); + } + + findAll(page?: number, pageSize?: number): Promise> { + const hasPagination = page !== -1 && pageSize !== -1; + const reservations = this.prisma.reservation.findMany({ + skip: hasPagination ? (page - 1) * pageSize : undefined, + take: hasPagination ? pageSize : undefined, + orderBy: { + startTime: 'asc', + }, + }); + + const count = this.prisma.reservation.count(); + + return Promise.all([reservations, count]) + .then(([reservations, count]) => { + const limit = hasPagination ? Math.floor(count / pageSize) : 0; + return { + data: reservations, + count, + page, + limit, + }; + }) + .catch(() => { + throw new InternalServerErrorException('An error occurred.'); + }); + } + + async findOne(id: number) { + try { + return await this.prisma.reservation.findUniqueOrThrow({ + where: { + id, + }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + if (e.code === 'P2025') { + throw new NotFoundException(`This reservation doesn't exist.`); + } + throw new InternalServerErrorException('An error occurred.'); + } + } + } + + async update(id: number, updateReservationDto: UpdateReservationDto) { + try { + return await this.prisma.reservation.update({ + where: { + id, + }, + data: { + ...updateReservationDto, + }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + if (e.code === 'P2025') { + throw new NotFoundException(`This reservation doesn't exist.`); + } + throw new InternalServerErrorException('An error occurred.'); + } + } + } + + remove(id: number) { + try { + return this.prisma.reservation.delete({ + where: { + id, + }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + if (e.code === 'P2025') { + throw new NotFoundException(`This reservation doesn't exist.`); + } + throw new InternalServerErrorException('An error occurred.'); + } + } + } +} diff --git a/yarn.lock b/yarn.lock index 7428f19..0fb07e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1037,6 +1037,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1732,6 +1737,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -4222,7 +4232,7 @@ streamsearch@^1.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@4.1.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.1.2: +string-width@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" integrity sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ== @@ -4231,6 +4241,24 @@ string-width@4.1.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2. is-fullwidth-code-point "^3.0.0" strip-ansi "^5.2.0" +string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.matchall@^4.0.10: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" @@ -4880,15 +4908,24 @@ which@^2.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@7.0.0, wrap-ansi@^6.0.1, wrap-ansi@^6.2.0, wrap-ansi@^8.1.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== +wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"