diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 0f6ac32..784cb78 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -4,9 +4,11 @@ import { PrismaModule } from './prisma/prisma.module' import { UsersModule } from './users/users.module' 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' @Module({ - imports: [PrismaModule, UsersModule, AuthModule, GeolocationsModule], + imports: [PrismaModule, UsersModule, AuthModule, GeolocationsModule, ChallengesModule, ChallengeActionsModule], providers: [PrismaService], }) export class AppModule {} diff --git a/server/src/challenge-actions/challenge-actions.controller.spec.ts b/server/src/challenge-actions/challenge-actions.controller.spec.ts new file mode 100644 index 0000000..6805bb2 --- /dev/null +++ b/server/src/challenge-actions/challenge-actions.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { ChallengeActionsController } from './challenge-actions.controller' +import { ChallengeActionsService } from './challenge-actions.service' + +describe('ChallengeActionsController', () => { + let controller: ChallengeActionsController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ChallengeActionsController], + providers: [ChallengeActionsService], + }).compile() + + controller = module.get(ChallengeActionsController) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) +}) diff --git a/server/src/challenge-actions/challenge-actions.controller.ts b/server/src/challenge-actions/challenge-actions.controller.ts new file mode 100644 index 0000000..6601880 --- /dev/null +++ b/server/src/challenge-actions/challenge-actions.controller.ts @@ -0,0 +1,76 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, HttpCode, UseGuards, Req, Query, NotFoundException } from '@nestjs/common' +import { ChallengeActionsService } from './challenge-actions.service' +import { AuthenticatedRequest, JwtAuthGuard } from 'src/auth/jwt-auth.guard' +import { ApiBearerAuth, ApiCreatedResponse, ApiForbiddenResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger' +import { ChallengeActionEntity } from './entities/challenge-action.entity' +import { CreateChallengeActionDto } from './dto/create-challenge-action.dto' +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' +import { UpdateChallengeActionDto } from './dto/update-challenge-action.dto' + +@Controller('challenge-actions') +export class ChallengeActionsController { + constructor(private readonly challengeActionsService: ChallengeActionsService) {} + + @Post() + @HttpCode(201) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiCreatedResponse({ type: ChallengeActionEntity, description: "Objet créé avec succès" }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + async create(@Req() request: AuthenticatedRequest, @Body() createChallengeActionDto: CreateChallengeActionDto): Promise { + const user = request.user + const challenge = await this.challengeActionsService.create(user, createChallengeActionDto) + return new ChallengeActionEntity(challenge) + } + + @Get() + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponsePaginated(ChallengeActionEntity) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + async findAll(@Query() queryPagination?: QueryPaginationDto): Promise> { + const [challengeActions, total] = await this.challengeActionsService.findAll(queryPagination) + return paginateOutput(challengeActions.map(challengeAction => new ChallengeActionEntity(challengeAction)), total, queryPagination) + } + + @Get(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: ChallengeActionEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async findOne(@Param('id', ParseIntPipe) id: number): Promise { + const challenge = await this.challengeActionsService.findOne(id) + if (!challenge) + throw new NotFoundException(`Défi inexistant avec l'identifiant ${id}`) + return new ChallengeActionEntity(challenge) + } + + @Patch(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: ChallengeActionEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async update(@Param('id', ParseIntPipe) id: number, @Body() updateChallengeActionDto: UpdateChallengeActionDto) { + return await this.challengeActionsService.update(id, updateChallengeActionDto) + } + + @Delete(':id') + @HttpCode(204) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: ChallengeActionEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async remove(@Param('id', ParseIntPipe) id: number) { + await this.challengeActionsService.remove(id) + } +} diff --git a/server/src/challenge-actions/challenge-actions.module.ts b/server/src/challenge-actions/challenge-actions.module.ts new file mode 100644 index 0000000..2eca631 --- /dev/null +++ b/server/src/challenge-actions/challenge-actions.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { ChallengeActionsService } from './challenge-actions.service' +import { ChallengeActionsController } from './challenge-actions.controller' +import { PrismaModule } from 'src/prisma/prisma.module' + +@Module({ + controllers: [ChallengeActionsController], + providers: [ChallengeActionsService], + imports: [PrismaModule], +}) +export class ChallengeActionsModule {} diff --git a/server/src/challenge-actions/challenge-actions.service.spec.ts b/server/src/challenge-actions/challenge-actions.service.spec.ts new file mode 100644 index 0000000..8439d38 --- /dev/null +++ b/server/src/challenge-actions/challenge-actions.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { ChallengeActionsService } from './challenge-actions.service' + +describe('ChallengeActionsService', () => { + let service: ChallengeActionsService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ChallengeActionsService], + }).compile() + + service = module.get(ChallengeActionsService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/server/src/challenge-actions/challenge-actions.service.ts b/server/src/challenge-actions/challenge-actions.service.ts new file mode 100644 index 0000000..0265143 --- /dev/null +++ b/server/src/challenge-actions/challenge-actions.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common' +import { CreateChallengeActionDto } from './dto/create-challenge-action.dto' +import { UpdateChallengeActionDto } from './dto/update-challenge-action.dto' +import { ChallengeAction, User } from '@prisma/client' +import { PrismaService } from 'src/prisma/prisma.service' +import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' +import { paginate } from 'src/common/utils/pagination.utils' + +@Injectable() +export class ChallengeActionsService { + constructor(private prisma: PrismaService) { } + + async create(authenticatedUser: User, createChallengeActionDto: CreateChallengeActionDto): Promise { + const data = { ...createChallengeActionDto, userId: authenticatedUser.id } + return await this.prisma.challengeAction.create({ data: data }) + } + + async findAll(queryPagination?: QueryPaginationDto): Promise<[ChallengeAction[], number]> { + return [ + await this.prisma.challengeAction.findMany({ + ...paginate(queryPagination), + }), + await this.prisma.challenge.count(), + ] + } + + async findOne(id: number): Promise { + return await this.prisma.challengeAction.findUnique({ where: { id } }) + } + + async update(id: number, updateChallengeActionDto: UpdateChallengeActionDto): Promise { + return await this.prisma.challengeAction.update({ + where: { id }, + data: updateChallengeActionDto, + }) + } + + async remove(id: number): Promise { + return await this.prisma.challengeAction.delete({ where: { id } }) + } +} diff --git a/server/src/challenge-actions/dto/create-challenge-action.dto.ts b/server/src/challenge-actions/dto/create-challenge-action.dto.ts new file mode 100644 index 0000000..50e0bcc --- /dev/null +++ b/server/src/challenge-actions/dto/create-challenge-action.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger" +import { Type } from "class-transformer" +import { IsBoolean, IsInt } from "class-validator" + +export class CreateChallengeActionDto { + @IsInt() + @Type(() => Number) + @ApiProperty({ description: "Identifiant du défi rattaché à l'action" }) + challengeId: number + + @IsBoolean() + @Type(() => Boolean) + @ApiProperty({ description: "Est-ce que le défi est actuellement en train d'être réalisé" }) + active: boolean + + @IsBoolean() + @Type(() => Boolean) + @ApiProperty({ description: "Est-ce que le défi a été réussi" }) + success: boolean +} diff --git a/server/src/challenge-actions/dto/update-challenge-action.dto.ts b/server/src/challenge-actions/dto/update-challenge-action.dto.ts new file mode 100644 index 0000000..d2caec1 --- /dev/null +++ b/server/src/challenge-actions/dto/update-challenge-action.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger' +import { CreateChallengeActionDto } from './create-challenge-action.dto' + +export class UpdateChallengeActionDto extends PartialType(CreateChallengeActionDto) {} diff --git a/server/src/challenge-actions/entities/challenge-action.entity.ts b/server/src/challenge-actions/entities/challenge-action.entity.ts new file mode 100644 index 0000000..3990d05 --- /dev/null +++ b/server/src/challenge-actions/entities/challenge-action.entity.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from "@nestjs/swagger" +import { ChallengeAction } from "@prisma/client" + +export class ChallengeActionEntity implements ChallengeAction { + constructor(partial: Partial) { + Object.assign(this, partial) + } + + @ApiProperty({ description: "Identifiant unique" }) + id: number + + @ApiProperty({ description: "Identifiant de l'utilisateur⋅rice effectuant le défi" }) + userId: number + + @ApiProperty({ description: "Identifiant du défi rattaché à l'action" }) + challengeId: number + + @ApiProperty({ description: "Est-ce que le défi est actuellement en train d'être réalisé" }) + active: boolean + + @ApiProperty({ description: "Est-ce que le défi a été réussi" }) + success: boolean +} diff --git a/server/src/challenges/challenges.controller.spec.ts b/server/src/challenges/challenges.controller.spec.ts new file mode 100644 index 0000000..dfbe9fa --- /dev/null +++ b/server/src/challenges/challenges.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { ChallengesController } from './challenges.controller' +import { ChallengesService } from './challenges.service' + +describe('ChallengesController', () => { + let controller: ChallengesController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ChallengesController], + providers: [ChallengesService], + }).compile() + + controller = module.get(ChallengesController) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) +}) diff --git a/server/src/challenges/challenges.controller.ts b/server/src/challenges/challenges.controller.ts new file mode 100644 index 0000000..7f35c09 --- /dev/null +++ b/server/src/challenges/challenges.controller.ts @@ -0,0 +1,75 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, HttpCode, UseGuards, Req, Query, NotFoundException } from '@nestjs/common' +import { ChallengesService } from './challenges.service' +import { CreateChallengeDto } from './dto/create-challenge.dto' +import { UpdateChallengeDto } from './dto/update-challenge.dto' +import { JwtAuthGuard } from 'src/auth/jwt-auth.guard' +import { ApiBearerAuth, ApiCreatedResponse, ApiForbiddenResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger' +import { ChallengeEntity } from './entities/challenge.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('challenges') +export class ChallengesController { + constructor(private readonly challengesService: ChallengesService) {} + + @Post() + @HttpCode(201) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiCreatedResponse({ type: ChallengeEntity, description: "Objet créé avec succès" }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + async create(@Body() createChallengeDto: CreateChallengeDto): Promise { + const challenge = await this.challengesService.create(createChallengeDto) + return new ChallengeEntity(challenge) + } + + @Get() + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponsePaginated(ChallengeEntity) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + async findAll(@Query() queryPagination?: QueryPaginationDto): Promise> { + const [challenges, total] = await this.challengesService.findAll(queryPagination) + return paginateOutput(challenges.map(challenge => new ChallengeEntity(challenge)), total, queryPagination) + } + + @Get(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: ChallengeEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async findOne(@Param('id', ParseIntPipe) id: number): Promise { + const challenge = await this.challengesService.findOne(id) + if (!challenge) + throw new NotFoundException(`Défi inexistant avec l'identifiant ${id}`) + return new ChallengeEntity(challenge) + } + + @Patch(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: ChallengeEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async update(@Param('id', ParseIntPipe) id: number, @Body() updateChallengeDto: UpdateChallengeDto) { + return await this.challengesService.update(id, updateChallengeDto) + } + + @Delete(':id') + @HttpCode(204) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({ type: ChallengeEntity }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + @ApiNotFoundResponse({ description: "Objet non trouvé" }) + async remove(@Param('id', ParseIntPipe) id: number) { + await this.challengesService.remove(id) + } +} diff --git a/server/src/challenges/challenges.module.ts b/server/src/challenges/challenges.module.ts new file mode 100644 index 0000000..4c73f27 --- /dev/null +++ b/server/src/challenges/challenges.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { ChallengesService } from './challenges.service' +import { ChallengesController } from './challenges.controller' +import { PrismaModule } from 'src/prisma/prisma.module' + +@Module({ + controllers: [ChallengesController], + providers: [ChallengesService], + imports: [PrismaModule], +}) +export class ChallengesModule {} diff --git a/server/src/challenges/challenges.service.spec.ts b/server/src/challenges/challenges.service.spec.ts new file mode 100644 index 0000000..4ef1436 --- /dev/null +++ b/server/src/challenges/challenges.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { ChallengesService } from './challenges.service' + +describe('ChallengesService', () => { + let service: ChallengesService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ChallengesService], + }).compile() + + service = module.get(ChallengesService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/server/src/challenges/challenges.service.ts b/server/src/challenges/challenges.service.ts new file mode 100644 index 0000000..6495a9e --- /dev/null +++ b/server/src/challenges/challenges.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common' +import { CreateChallengeDto } from './dto/create-challenge.dto' +import { UpdateChallengeDto } from './dto/update-challenge.dto' +import { Challenge } from '@prisma/client' +import { PrismaService } from 'src/prisma/prisma.service' +import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' +import { paginate } from 'src/common/utils/pagination.utils' + +@Injectable() +export class ChallengesService { + constructor(private prisma: PrismaService) { } + + async create(createChallengeDto: CreateChallengeDto): Promise { + const data = { ...createChallengeDto } + return await this.prisma.challenge.create({ data: data }) + } + + async findAll(queryPagination?: QueryPaginationDto): Promise<[Challenge[], number]> { + return [ + await this.prisma.challenge.findMany({ + ...paginate(queryPagination), + }), + await this.prisma.challenge.count(), + ] + } + + async findOne(id: number): Promise { + return await this.prisma.challenge.findUnique({ where: { id } }) + } + + async update(id: number, updateChallengeDto: UpdateChallengeDto): Promise { + return await this.prisma.challenge.update({ + where: { id }, + data: updateChallengeDto, + }) + } + + async remove(id: number): Promise { + return await this.prisma.challenge.delete({ where: { id } }) + } +} diff --git a/server/src/challenges/dto/create-challenge.dto.ts b/server/src/challenges/dto/create-challenge.dto.ts new file mode 100644 index 0000000..2523dac --- /dev/null +++ b/server/src/challenges/dto/create-challenge.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from "@nestjs/swagger" +import { Type } from "class-transformer" +import { IsInt, IsString } from "class-validator" + +export class CreateChallengeDto { + @IsString() + @ApiProperty({ description: "Titre du défi" }) + title: string + + @IsString() + @ApiProperty({ description: "Description du défi, de l'action à réaliser" }) + description: string + + @IsInt() + @Type(() => Number) + @ApiProperty({ description: "Récompense en nombre de points en cas de réussite de læ joueur⋅se" }) + reward: number +} diff --git a/server/src/challenges/dto/update-challenge.dto.ts b/server/src/challenges/dto/update-challenge.dto.ts new file mode 100644 index 0000000..4400ac0 --- /dev/null +++ b/server/src/challenges/dto/update-challenge.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger' +import { CreateChallengeDto } from './create-challenge.dto' + +export class UpdateChallengeDto extends PartialType(CreateChallengeDto) {} diff --git a/server/src/challenges/entities/challenge.entity.ts b/server/src/challenges/entities/challenge.entity.ts new file mode 100644 index 0000000..e5d3f3d --- /dev/null +++ b/server/src/challenges/entities/challenge.entity.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger" +import { Challenge } from "@prisma/client" + +export class ChallengeEntity implements Challenge { + constructor(partial: Partial) { + Object.assign(this, partial) + } + + @ApiProperty({ description: "Identifiant unique" }) + id: number + + @ApiProperty({ description: "Titre du challenger" }) + title: string + + @ApiProperty({ description: "Description du challenge, de l'action à réaliser" }) + description: string + + @ApiProperty({ description: "Récompense en nombre de points en cas de réussite du challenger" }) + reward: number +} diff --git a/server/src/common/dto/user_filter.dto.ts b/server/src/common/dto/user_filter.dto.ts index 8835647..58a7797 100644 --- a/server/src/common/dto/user_filter.dto.ts +++ b/server/src/common/dto/user_filter.dto.ts @@ -1,5 +1,5 @@ -import { Type } from "class-transformer"; -import { IsNumber, IsOptional } from "class-validator"; +import { Type } from "class-transformer" +import { IsNumber, IsOptional } from "class-validator" export class UserFilterDto { @IsOptional() diff --git a/server/src/geolocations/geolocations.controller.ts b/server/src/geolocations/geolocations.controller.ts index 2eefed2..d1a1247 100644 --- a/server/src/geolocations/geolocations.controller.ts +++ b/server/src/geolocations/geolocations.controller.ts @@ -20,7 +20,6 @@ export class GeolocationsController { @ApiCreatedResponse({ type: GeolocationEntity, description: "Objet créé avec succès" }) @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) @ApiForbiddenResponse({ description: "Permission refusée" }) - @ApiNotFoundResponse({ description: "Object non trouvé" }) async create(@Req() request: AuthenticatedRequest, @Body() createGeolocationDto: CreateGeolocationDto): Promise { const user = request.user const geolocation = await this.geolocationsService.create(user, createGeolocationDto) @@ -33,7 +32,6 @@ export class GeolocationsController { @ApiOkResponsePaginated(GeolocationEntity) @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) @ApiForbiddenResponse({ description: "Permission refusée" }) - @ApiNotFoundResponse({ description: "Objet non trouvé" }) async findAll(@Query() queryPagination?: QueryPaginationDto, @Query() userFilter?: UserFilterDto): Promise> { const [geolocations, total] = await this.geolocationsService.findAll(queryPagination, userFilter) return paginateOutput(geolocations.map(geolocation => new GeolocationEntity(geolocation)), total, queryPagination) diff --git a/server/src/geolocations/geolocations.service.ts b/server/src/geolocations/geolocations.service.ts index 9757a95..80596a8 100644 --- a/server/src/geolocations/geolocations.service.ts +++ b/server/src/geolocations/geolocations.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common' import { CreateGeolocationDto } from './dto/create-geolocation.dto' import { PrismaService } from 'src/prisma/prisma.service' -import { Geolocation, User } from '@prisma/client' +import { Geolocation, Prisma, User } from '@prisma/client' import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' import { paginate } from 'src/common/utils/pagination.utils' import { UserFilterDto } from 'src/common/dto/user_filter.dto' @@ -17,7 +17,8 @@ export class GeolocationsService { async findAll(queryPagination?: QueryPaginationDto, userFilter?: UserFilterDto): Promise<[Geolocation[], number]> { const filter = { - where: userFilter?.userId ? { userId: userFilter.userId } : {} + where: (userFilter?.userId ? { userId: userFilter.userId } : {}), + orderBy: { timestamp: Prisma.SortOrder.desc }, } return [ await this.prisma.geolocation.findMany({ @@ -35,7 +36,7 @@ export class GeolocationsService { async findLastLocation(userId: number): Promise { return await this.prisma.geolocation.findFirst({ where: { userId: userId }, - orderBy: { timestamp: "desc" }, + orderBy: { timestamp: Prisma.SortOrder.desc }, take: 1 }) }