From c083dce79f880de8f481da48630848880aeb173e Mon Sep 17 00:00:00 2001 From: Guillaume DEMAN Date: Thu, 23 May 2024 13:55:36 +0200 Subject: [PATCH 1/6] Handle ZodError --- src/app/utils/errorHandlerUtils.ts | 61 +++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/app/utils/errorHandlerUtils.ts b/src/app/utils/errorHandlerUtils.ts index da9c2d6..1f7e135 100644 --- a/src/app/utils/errorHandlerUtils.ts +++ b/src/app/utils/errorHandlerUtils.ts @@ -6,7 +6,17 @@ import { NotFoundException, InternalServerErrorException, } from "@/types/exceptions"; -import { ZodError } from "zod"; +import { + ZodError, + ZodInvalidEnumValueIssue, + ZodInvalidLiteralIssue, + ZodInvalidStringIssue, + ZodInvalidTypeIssue, + ZodIssueCode, + ZodTooBigIssue, + ZodTooSmallIssue, + ZodUnrecognizedKeysIssue, +} from "zod"; /** * @params error: any @@ -17,8 +27,10 @@ export function handleException(error: any) { console.error("An error occurred:", error); switch (true) { - case error instanceof BadRequestException: case error instanceof ZodError: + const formattedMessage = formatZodErrors(error); + return NextResponse.json({ message: formattedMessage }, { status: 400 }); + case error instanceof BadRequestException: return NextResponse.json({ message: error.message }, { status: 400 }); case error instanceof UnauthorizedException: return NextResponse.json({ message: error.message }, { status: 401 }); @@ -35,3 +47,48 @@ export function handleException(error: any) { ); } } + +function formatZodErrors(error: ZodError): string { + return error.errors + .map((e) => { + const path = e.path.join(" -> "); + let message = `Error at ${path}: ${e.message}`; + + // Handle specific issue types + switch (e.code) { + case ZodIssueCode.invalid_type: + const invalidTypeIssue = e as ZodInvalidTypeIssue; + message += ` (expected ${invalidTypeIssue.expected}, received ${invalidTypeIssue.received})`; + break; + case ZodIssueCode.invalid_literal: + const invalidLiteralIssue = e as ZodInvalidLiteralIssue; + message += ` (expected ${invalidLiteralIssue.expected}, received ${invalidLiteralIssue.received})`; + break; + case ZodIssueCode.too_small: + const tooSmallIssue = e as ZodTooSmallIssue; + message += ` (minimum ${tooSmallIssue.minimum}, inclusive: ${tooSmallIssue.inclusive})`; + break; + case ZodIssueCode.too_big: + const tooBigIssue = e as ZodTooBigIssue; + message += ` (maximum ${tooBigIssue.maximum}, inclusive: ${tooBigIssue.inclusive})`; + break; + case ZodIssueCode.invalid_enum_value: + const enumIssue = e as ZodInvalidEnumValueIssue; + message += ` (expected one of: ${enumIssue.options.join( + ", " + )}, received: ${enumIssue.received})`; + break; + case ZodIssueCode.unrecognized_keys: + const keysIssue = e as ZodUnrecognizedKeysIssue; + message += ` (unrecognized keys: ${keysIssue.keys.join(", ")})`; + break; + case ZodIssueCode.invalid_string: + const stringIssue = e as ZodInvalidStringIssue; + message += ` (validation: ${stringIssue.validation})`; + break; + } + + return message; + }) + .join("; "); +} From 6e9fbddd49290bd545920c39bbc1da03887928a7 Mon Sep 17 00:00:00 2001 From: Guillaume DEMAN Date: Fri, 24 May 2024 08:40:35 +0200 Subject: [PATCH 2/6] wip : zod integration in event comment journey step --- src/app/api/comments/[id]/route.ts | 5 +- src/app/api/events/[id]/route.ts | 13 ++++-- src/app/api/events/route.ts | 7 ++- src/app/utils/afficherTypesDesAttributs.ts | 35 ++++++++++++++ src/repositories/eventRepository.ts | 6 +-- src/services/eventService.ts | 5 +- src/types/event.ts | 19 ++++++++ src/types/eventWithUserEvents.ts | 5 -- .../{journeyWithComments.ts => journey.ts} | 4 ++ src/types/journeyWithSteps.ts | 5 -- src/validators/api/eventSchema.ts | 46 +++++++++++++------ 11 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 src/app/utils/afficherTypesDesAttributs.ts create mode 100644 src/types/event.ts delete mode 100644 src/types/eventWithUserEvents.ts rename src/types/{journeyWithComments.ts => journey.ts} (60%) delete mode 100644 src/types/journeyWithSteps.ts diff --git a/src/app/api/comments/[id]/route.ts b/src/app/api/comments/[id]/route.ts index b412856..4a1c418 100644 --- a/src/app/api/comments/[id]/route.ts +++ b/src/app/api/comments/[id]/route.ts @@ -5,6 +5,7 @@ import { removeComment, } from "@/services/commentService"; import { CommentWithoutDates } from "@/types/CommentWithoutDates"; +import { commentBodySchema } from "@/validators/api/commentSchema"; import { NextRequest, NextResponse } from "next/server"; /** @@ -39,9 +40,9 @@ export async function PUT( try { const id: number = Number(params.id); const body = await request.json(); - const comment: CommentWithoutDates = body.comment; + const commentParsed = commentBodySchema.parse(body); - console.log("COMMENT : " + comment); + const comment: CommentWithoutDates = commentParsed.comment; const result = await registerOrModifyComment(id, comment); return NextResponse.json({ data: result }, { status: 200 }); diff --git a/src/app/api/events/[id]/route.ts b/src/app/api/events/[id]/route.ts index cfa051e..0f900b0 100644 --- a/src/app/api/events/[id]/route.ts +++ b/src/app/api/events/[id]/route.ts @@ -1,10 +1,12 @@ +import { afficherTypesDesAttributs } from "@/app/utils/afficherTypesDesAttributs"; import { handleException } from "@/app/utils/errorHandlerUtils"; import { getEventByIdWithUserEvents, registerOrModifyEvent, removeEvent, } from "@/services/eventService"; -import { Event } from "@prisma/client"; +import { eventWithoutId } from "@/types/event"; +import { eventBodySchema } from "@/validators/api/eventSchema"; import { NextRequest, NextResponse } from "next/server"; /** @@ -38,8 +40,13 @@ export async function PUT( ) { try { const id: number = Number(params.id); - const body = await request.json(); - const event: Event = body.event; + let body = await request.json(); + let bodytarget: object; + afficherTypesDesAttributs(body.event, EventWithoutId); + + const eventParsed = eventBodySchema.parse(body); + + const event: eventWithoutId = eventParsed.event; const result = await registerOrModifyEvent(id, event); return NextResponse.json({ data: result }, { status: 200 }); diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index ba6500b..f2274a3 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -1,5 +1,7 @@ import { handleException } from "@/app/utils/errorHandlerUtils"; import { getAllEvents, registerOrModifyEvent } from "@/services/eventService"; +import { eventWithoutId } from "@/types/event"; +import { eventBodySchema } from "@/validators/api/eventSchema"; import { Event } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; @@ -24,8 +26,11 @@ export async function GET() { export async function POST(request: NextRequest) { try { const body = await request.json(); - const event: Event = body.event; + body.event.startAt = new Date(body.event.startAt); + body.event.endAt = new Date(body.event.endAt); + const eventParsed = eventBodySchema.parse(body); + const event: eventWithoutId = eventParsed.event; const result = await registerOrModifyEvent(null, event); return NextResponse.json({ data: result }, { status: 200 }); } catch (error: any) { diff --git a/src/app/utils/afficherTypesDesAttributs.ts b/src/app/utils/afficherTypesDesAttributs.ts new file mode 100644 index 0000000..b5cbc69 --- /dev/null +++ b/src/app/utils/afficherTypesDesAttributs.ts @@ -0,0 +1,35 @@ +// Utilitaire pour obtenir les types des attributs de T +type TypeDesAttributs = { + [K in keyof T]: T[K]; +}; + +// Fonction pour afficher les types des attributs de T +export function afficherTypesDesAttributs( + body: T, + ctor: new () => T +) { + console.log("AFFICHER TYPES DES ATTRIBUTS"); + + // Instancier un nouvel objet de type T + const bodyTypeTarget = new ctor(); + + // Obtenir les clés de l'objet (y compris les symboles) + const keys = [ + ...Object.keys(bodyTypeTarget), + ...Object.getOwnPropertySymbols(bodyTypeTarget), + ] as (keyof T)[]; + + // Créer un objet pour stocker les types des attributs + const types: Record = {}; + + // Parcourir les clés et obtenir les types + keys.forEach((key) => { + const keyAsString = String(key); // Convertir la clé en chaîne de caractères + const valeur = body[key as keyof T]; + types[keyAsString] = typeof valeur; + console.log(`Clé: ${keyAsString}, Type: ${typeof valeur}`); + }); + + // Retourner les types des attributs + return types; +} diff --git a/src/repositories/eventRepository.ts b/src/repositories/eventRepository.ts index 28c55dc..b1b3392 100644 --- a/src/repositories/eventRepository.ts +++ b/src/repositories/eventRepository.ts @@ -1,5 +1,5 @@ import prisma from "@/lib/prisma"; -import { eventWithUserEvents } from "@/types/eventWithUserEvents"; +import { eventWithUserEvents, eventWithoutId } from "@/types/event"; import { Event } from "@prisma/client"; /** @@ -7,7 +7,7 @@ import { Event } from "@prisma/client"; * @returns Event * @description Creates a new event with the provided data. */ -export const createEvent = async (data: Event): Promise => { +export const createEvent = async (data: eventWithoutId): Promise => { return await prisma.event.create({ data, }); @@ -45,7 +45,7 @@ export const readEvents = async (): Promise => { */ export const updateEvent = async ( id: number, - data: Event + data: eventWithoutId ): Promise => { return await prisma.event.update({ where: { id }, diff --git a/src/services/eventService.ts b/src/services/eventService.ts index 8884bb1..65f4e29 100644 --- a/src/services/eventService.ts +++ b/src/services/eventService.ts @@ -11,12 +11,13 @@ import { deleteEvent, } from "../repositories/eventRepository"; import { Event, UserEvent } from "@prisma/client"; -import { eventWithUserEvents } from "@/types/eventWithUserEvents"; import { createUserEvent, deleteUserEvent, readUserEventByUserIdAndEventId, } from "@/repositories/userEventRepository"; +import { eventWithUserEvents, eventWithoutId } from "@/types/event"; +import { UserEventWithoutId } from "@/types/userEventWithoutId"; /** * @params id: number @@ -58,7 +59,7 @@ export const getAllEvents = async (): Promise => { */ export const registerOrModifyEvent = async ( id: number | null, - event: Event + event: eventWithoutId ): Promise => { // Check arguments if (id !== null && !Number.isFinite(id)) { diff --git a/src/types/event.ts b/src/types/event.ts new file mode 100644 index 0000000..4d68dbb --- /dev/null +++ b/src/types/event.ts @@ -0,0 +1,19 @@ +import { Prisma } from "@prisma/client"; + +export type eventWithUserEvents = Prisma.EventGetPayload<{ + include: { userEvents: true }; +}>; + +export type eventWithoutId = { + authorId: number; + journeyId: number; + title: string; + image: string; + numberPlayerMin: number; + numberPlayerMax: number; + description: string; + startAt: Date; + endAt: Date; + isPrivate?: boolean; + accessCode?: string; +}; diff --git a/src/types/eventWithUserEvents.ts b/src/types/eventWithUserEvents.ts deleted file mode 100644 index 5512d69..0000000 --- a/src/types/eventWithUserEvents.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Prisma } from "@prisma/client"; - -export type eventWithUserEvents = Prisma.EventGetPayload<{ - include: { userEvents: true }; -}>; diff --git a/src/types/journeyWithComments.ts b/src/types/journey.ts similarity index 60% rename from src/types/journeyWithComments.ts rename to src/types/journey.ts index 914cf1f..849f4c7 100644 --- a/src/types/journeyWithComments.ts +++ b/src/types/journey.ts @@ -3,3 +3,7 @@ import { Prisma } from "@prisma/client"; export type journeyWithComments = Prisma.JourneyGetPayload<{ include: { comments: true }; }>; + +export type journeyWithSteps = Prisma.JourneyGetPayload<{ + include: { steps: true }; +}>; diff --git a/src/types/journeyWithSteps.ts b/src/types/journeyWithSteps.ts deleted file mode 100644 index 4e57e85..0000000 --- a/src/types/journeyWithSteps.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Prisma } from "@prisma/client"; - -export type journeyWithSteps = Prisma.JourneyGetPayload<{ - include: { steps: true }; -}>; diff --git a/src/validators/api/eventSchema.ts b/src/validators/api/eventSchema.ts index 58b412f..e481c8f 100644 --- a/src/validators/api/eventSchema.ts +++ b/src/validators/api/eventSchema.ts @@ -1,18 +1,38 @@ import { z } from "zod"; -// Event schema -export const eventSchema = z.object({ - authorId: z.number().int(), - journeyId: z.number().int(), - title: z.string(), - image: z.string().url(), - numberPlayerMin: z.number().int(), - numberPlayerMax: z.number().int(), - description: z.string(), - isPrivate: z.boolean().optional(), - accessCode: z.string().nullable().optional(), - startAt: z.string().datetime(), // Using z.string() for ISO date strings - endAt: z.string().datetime(), +// Event schema de base avec des messages d'erreur personnalisés +const baseEventSchema = z.object({ + authorId: z.number({ required_error: "Ce champ est requis" }).int(), + journeyId: z.number({ required_error: "Ce champ est requis" }).int(), + title: z + .string({ required_error: "Ce champ est requis" }) + .max(255, { message: "Veuillez renseigner moins de 255 caractères" }), + image: z + .string({ required_error: "Ce champ est requis" }) + .url({ message: "Veuillez fournir une URL valide" }), + numberPlayerMin: z + .number({ required_error: "Ce champ est requis" }) + .int({ message: "Veuillez fournir un nombre entier" }), + numberPlayerMax: z + .number({ required_error: "Ce champ est requis" }) + .int({ message: "Veuillez fournir un nombre entier" }), + description: z + .string({ required_error: "Ce champ est requis" }) + .max(500, { message: "Veuillez renseigner moins de 500 caractères" }), + isPrivate: z.boolean({ required_error: "Ce champ est requis" }), + accessCode: z.string().optional(), + startAt: z.date({ required_error: "Ce champ est requis" }), + endAt: z.date({ required_error: "Ce champ est requis" }), +}); + +export const eventSchema = baseEventSchema.superRefine((data, ctx) => { + if (data.isPrivate && (!data.accessCode || data.accessCode.trim() === "")) { + ctx.addIssue({ + code: "custom", + message: "accessCode is required when isPrivate is true", + path: ["accessCode"], + }); + } }); // Combined schema for the body From 5d8613e2ff877e88e158966fc4397aad10197e3f Mon Sep 17 00:00:00 2001 From: Guillaume DEMAN Date: Sun, 23 Jun 2024 20:09:23 +0200 Subject: [PATCH 3/6] Update schema, services features --- .../migration.sql | 18 +- prisma/schema.prisma | 105 ++++---- prisma/seed.ts | 45 ++-- src/app/api/auth/[...nextauth]/route.ts | 4 +- src/app/api/comments/[id]/route.ts | 7 +- src/app/api/comments/route.ts | 7 +- .../api/events/[id]/join/[userId]/route.ts | 4 +- .../api/events/[id]/leave/[userId]/route.ts | 7 +- src/app/api/events/[id]/route.ts | 12 +- .../[id]/user/[userId]/step/[stepId]/route.ts | 0 .../user/[userId]/step/[stepId]/route.ts | 50 ++++ .../[eventId]/user/[userId]/steps/route.ts | 27 +++ src/app/api/events/route.ts | 10 +- src/app/api/journeys/[id]/route.ts | 10 +- src/app/api/journeys/route.ts | 10 +- src/app/api/steps/[id]/route.ts | 6 +- src/app/api/steps/route.ts | 9 +- src/app/utils/afficherTypesDesAttributs.ts | 35 --- src/app/utils/errorHandlerUtils.ts | 5 +- src/app/utils/utils.ts | 41 ++++ src/repositories/eventRepository.ts | 10 +- .../eventUserStepuserStepRepository.ts | 94 ++++++++ src/repositories/journeyRepository.ts | 140 ++++++----- src/repositories/stepRepository.ts | 5 +- src/repositories/userEventRepository.ts | 2 +- src/services/commentService.ts | 2 +- src/services/eventService.ts | 228 ++++++++++++++++-- src/services/journeyService.ts | 110 ++++++--- src/services/stepService.ts | 60 +++-- .../{CommentWithoutDates.ts => comment.ts} | 0 src/types/enums/timeSeparator.ts | 5 + src/types/event.ts | 4 +- src/types/eventUserStep.ts | 8 + src/types/journey.ts | 22 +- src/types/step.ts | 16 ++ .../{userEventWithoutId.ts => userEvent.ts} | 0 src/validators/api/commentSchema.ts | 18 +- src/validators/api/eventSchema.ts | 38 +-- src/validators/api/journeySchema.ts | 83 +++++-- src/validators/api/stepSchema.ts | 58 +++-- 40 files changed, 956 insertions(+), 359 deletions(-) rename prisma/migrations/{20240523072139_init => 20240623174056_init}/migration.sql (87%) create mode 100644 src/app/api/events/[id]/user/[userId]/step/[stepId]/route.ts create mode 100644 src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/step/[stepId]/route.ts create mode 100644 src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/steps/route.ts delete mode 100644 src/app/utils/afficherTypesDesAttributs.ts create mode 100644 src/app/utils/utils.ts create mode 100644 src/repositories/eventUserStepuserStepRepository.ts rename src/types/{CommentWithoutDates.ts => comment.ts} (100%) create mode 100644 src/types/enums/timeSeparator.ts create mode 100644 src/types/eventUserStep.ts create mode 100644 src/types/step.ts rename src/types/{userEventWithoutId.ts => userEvent.ts} (100%) diff --git a/prisma/migrations/20240523072139_init/migration.sql b/prisma/migrations/20240623174056_init/migration.sql similarity index 87% rename from prisma/migrations/20240523072139_init/migration.sql rename to prisma/migrations/20240623174056_init/migration.sql index 8cc67bb..d4967bc 100644 --- a/prisma/migrations/20240523072139_init/migration.sql +++ b/prisma/migrations/20240623174056_init/migration.sql @@ -109,15 +109,16 @@ CREATE TABLE "Step" ( ); -- CreateTable -CREATE TABLE "UserStep" ( +CREATE TABLE "EventUserStep" ( "id" SERIAL NOT NULL, "userId" INTEGER NOT NULL, "stepId" INTEGER NOT NULL, - "startAt" TIMESTAMP(3) NOT NULL, - "endAt" TIMESTAMP(3) NOT NULL, - "duration" INTEGER NOT NULL, + "eventId" INTEGER NOT NULL, + "startAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "endAt" TIMESTAMP(3), + "durationMs" INTEGER, - CONSTRAINT "UserStep_pkey" PRIMARY KEY ("id") + CONSTRAINT "EventUserStep_pkey" PRIMARY KEY ("id") ); -- CreateTable @@ -161,10 +162,13 @@ ALTER TABLE "Journey" ADD CONSTRAINT "Journey_authorId_fkey" FOREIGN KEY ("autho ALTER TABLE "Step" ADD CONSTRAINT "Step_journeyId_fkey" FOREIGN KEY ("journeyId") REFERENCES "Journey"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "UserStep" ADD CONSTRAINT "UserStep_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "EventUserStep" ADD CONSTRAINT "EventUserStep_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EventUserStep" ADD CONSTRAINT "EventUserStep_stepId_fkey" FOREIGN KEY ("stepId") REFERENCES "Step"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "UserStep" ADD CONSTRAINT "UserStep_stepId_fkey" FOREIGN KEY ("stepId") REFERENCES "Step"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "EventUserStep" ADD CONSTRAINT "EventUserStep_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d0486e2..1457435 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,24 +21,24 @@ model Role { } model User { - id Int @id @default(autoincrement()) - name String? - lastName String? - email String @unique - username String - password String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - isVerified Boolean @default(false) - dateOfBirth DateTime? - avatar String? - experience Int @default(0) - userRoles UserRole[] // liste des rôles de l'utilisateur - userEvents UserEvent[] // liste des events auxquels l'utilisateur participe - userSteps UserStep[] // liste des étapes réalisées par l'utilisateur - comments Comment[] // liste des commentaires créés par l'utilisateur - journey Journey[] // liste des parcours créés par l'utilisateur - events Event[] // liste des events créés par l'utilisateur + id Int @id @default(autoincrement()) + name String? + lastName String? + email String @unique + username String + password String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + isVerified Boolean @default(false) + dateOfBirth DateTime? + avatar String? + experience Int @default(0) + userRoles UserRole[] // liste des rôles de l'utilisateur + userEvents UserEvent[] // liste des events auxquels l'utilisateur participe + EventUserSteps EventUserStep[] // liste des étapes réalisées par l'utilisateur + comments Comment[] // liste des commentaires créés par l'utilisateur + journey Journey[] // liste des parcours créés par l'utilisateur + events Event[] // liste des events créés par l'utilisateur } model UserRole { @@ -50,7 +50,7 @@ model UserRole { } model Event { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) authorId Int journeyId Int title String @@ -58,15 +58,16 @@ model Event { numberPlayerMin Int numberPlayerMax Int description String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - isPrivate Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + isPrivate Boolean @default(false) accessCode String? startAt DateTime endAt DateTime userEvents UserEvent[] - author User @relation(fields: [authorId], references: [id], onDelete: Cascade) - journey Journey @relation(fields: [journeyId], references: [id], onDelete: Cascade) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + journey Journey @relation(fields: [journeyId], references: [id], onDelete: Cascade) + EventUserStep EventUserStep[] } model UserEvent { @@ -102,35 +103,37 @@ model Journey { } model Step { - id Int @id @default(autoincrement()) - journeyId Int - puzzle String - answer String - hint String - picturePuzzle String? - pictureHint String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - latitude Float - longitude Float - address String? - city String? - postalCode String? - country String? - stepNumber Int - journey Journey @relation(fields: [journeyId], references: [id], onDelete: Cascade) - userSteps UserStep[] + id Int @id @default(autoincrement()) + journeyId Int + puzzle String + answer String + hint String + picturePuzzle String? + pictureHint String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + latitude Float + longitude Float + address String? + city String? + postalCode String? + country String? + stepNumber Int + journey Journey @relation(fields: [journeyId], references: [id], onDelete: Cascade) + EventUserSteps EventUserStep[] } -model UserStep { - id Int @id @default(autoincrement()) - userId Int - stepId Int - startAt DateTime - endAt DateTime - duration Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - step Step @relation(fields: [stepId], references: [id], onDelete: Cascade) +model EventUserStep { + id Int @id @default(autoincrement()) + userId Int + stepId Int + eventId Int + startAt DateTime @default(now()) + endAt DateTime? + durationMs Int? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + step Step @relation(fields: [stepId], references: [id], onDelete: Cascade) + event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) } model Comment { diff --git a/prisma/seed.ts b/prisma/seed.ts index bb2a87a..641040e 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1039,7 +1039,7 @@ async function main() { // Generate a random number of events for the user between 0 and 6 let numEvents = Math.floor(Math.random() * 7); - // Ensure that Bob, Henry, and Grace are registered for at least one event because we create userStep for this users + // Ensure that Bob, Henry, and Grace are registered for at least one event because we create EventUserStep for these users if ( users[i].id === bob.id || users[i].id === henry.id || @@ -1258,29 +1258,27 @@ async function main() { select: { eventId: true }, }); - const generateUserSteps = async (userId: number, eventIds: number[]) => { - const journeyIds = new Set(); - + const generateEventUserStep = async (userId: number, eventIds: number[]) => { for (const eventId of eventIds) { - const event = await prisma.event.findUnique({ where: { id: eventId } }); + const event = await prisma.event.findUnique({ + where: { id: eventId }, + select: { journeyId: true }, + }); if (!event) continue; - journeyIds.add(event.journeyId); - } - - const journeys = await prisma.journey.findMany({ - where: { id: { in: Array.from(journeyIds) } }, - select: { id: true, steps: true }, - }); + const journey = await prisma.journey.findUnique({ + where: { id: event.journeyId }, + select: { id: true, steps: true }, + }); - for (const journey of journeys) { - let numberOfStepsBeforeEnd: number = 0; + if (!journey) continue; - if (userId === bob.id) numberOfStepsBeforeEnd = 0; - if (userId === grace.id) numberOfStepsBeforeEnd = 1; - if (userId === henry.id) numberOfStepsBeforeEnd = 2; + // Randomize the number of steps completed by the user for this event + const numberOfStepsToComplete = Math.floor( + Math.random() * journey.steps.length + ); - for (let i = 0; i < journey.steps.length - numberOfStepsBeforeEnd; i++) { + for (let i = 0; i < numberOfStepsToComplete; i++) { const step = journey.steps[i]; const startAt = new Date(); // Randomly generate a duration between 30 minutes and 2 hours for each step @@ -1293,28 +1291,29 @@ async function main() { ); const duration = endAt.getTime() - startAt.getTime(); - await prisma.userStep.create({ + await prisma.eventUserStep.create({ data: { userId: userId, stepId: step.id, + eventId: eventId, startAt: startAt, endAt: endAt, - duration: duration, + durationMs: duration, }, }); } } }; - await generateUserSteps( + await generateEventUserStep( bob.id, bobEvents.map((event) => event.eventId) ); - await generateUserSteps( + await generateEventUserStep( henry.id, henryEvents.map((event) => event.eventId) ); - await generateUserSteps( + await generateEventUserStep( grace.id, graceEvents.map((event) => event.eventId) ); diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index fbddf57..9a271c8 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,6 +1,8 @@ import NextAuth from "next-auth"; import { authOptions } from "@/lib/authOptions"; +// TODO +// @ts-ignore const handler = NextAuth(authOptions); -export { handler as GET, handler as POST }; \ No newline at end of file +export { handler as GET, handler as POST }; diff --git a/src/app/api/comments/[id]/route.ts b/src/app/api/comments/[id]/route.ts index 4a1c418..7151ef3 100644 --- a/src/app/api/comments/[id]/route.ts +++ b/src/app/api/comments/[id]/route.ts @@ -4,7 +4,7 @@ import { registerOrModifyComment, removeComment, } from "@/services/commentService"; -import { CommentWithoutDates } from "@/types/CommentWithoutDates"; +import { CommentWithoutDates } from "@/types/comment"; import { commentBodySchema } from "@/validators/api/commentSchema"; import { NextRequest, NextResponse } from "next/server"; @@ -40,9 +40,8 @@ export async function PUT( try { const id: number = Number(params.id); const body = await request.json(); - const commentParsed = commentBodySchema.parse(body); - - const comment: CommentWithoutDates = commentParsed.comment; + // Parse the body with zod to get the comment + const comment: CommentWithoutDates = commentBodySchema.parse(body).comment; const result = await registerOrModifyComment(id, comment); return NextResponse.json({ data: result }, { status: 200 }); diff --git a/src/app/api/comments/route.ts b/src/app/api/comments/route.ts index 25a8659..f988956 100644 --- a/src/app/api/comments/route.ts +++ b/src/app/api/comments/route.ts @@ -1,6 +1,6 @@ import { handleException } from "@/app/utils/errorHandlerUtils"; import { registerOrModifyComment } from "@/services/commentService"; -import { CommentWithoutDates } from "@/types/CommentWithoutDates"; +import { CommentWithoutDates } from "@/types/comment"; import { commentBodySchema } from "@/validators/api/commentSchema"; import { NextRequest, NextResponse } from "next/server"; @@ -12,9 +12,8 @@ import { NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { try { const body = await request.json(); - const commentParsed = commentBodySchema.parse(body); - - const comment: CommentWithoutDates = commentParsed.comment; + // Parse the body with zod to get the comment + const comment: CommentWithoutDates = commentBodySchema.parse(body).comment; const result = await registerOrModifyComment(null, comment); return NextResponse.json({ data: result }, { status: 201 }); diff --git a/src/app/api/events/[id]/join/[userId]/route.ts b/src/app/api/events/[id]/join/[userId]/route.ts index 5ff4e11..b6baebf 100644 --- a/src/app/api/events/[id]/join/[userId]/route.ts +++ b/src/app/api/events/[id]/join/[userId]/route.ts @@ -13,10 +13,10 @@ export async function POST( { params }: { params: { id: string; userId: string } } ) { try { - const id: number = Number(params.id); + const eventId: number = Number(params.id); const userId: number = Number(params.userId); - const result = await joinEvent(id, userId); + const result = await joinEvent(eventId, userId); return NextResponse.json({ data: result }, { status: 200 }); } catch (error: any) { return handleException(error); diff --git a/src/app/api/events/[id]/leave/[userId]/route.ts b/src/app/api/events/[id]/leave/[userId]/route.ts index f10e4dd..354e030 100644 --- a/src/app/api/events/[id]/leave/[userId]/route.ts +++ b/src/app/api/events/[id]/leave/[userId]/route.ts @@ -1,6 +1,6 @@ import { handleException } from "@/app/utils/errorHandlerUtils"; import { leaveEvent } from "@/services/eventService"; -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; /** * @params request: NextRequest @@ -13,10 +13,11 @@ export async function DELETE( { params }: { params: { id: string; userId: string } } ) { try { - const id: number = Number(params.id); + const eventId: number = Number(params.id); const userId: number = Number(params.userId); - const result = await leaveEvent(id, userId); + const result = await leaveEvent(eventId, userId); + // Using Response instead of NextResponse because NextResponse doesn't handle status 204 actually return new Response(null, { status: 204, diff --git a/src/app/api/events/[id]/route.ts b/src/app/api/events/[id]/route.ts index 0f900b0..17d6c59 100644 --- a/src/app/api/events/[id]/route.ts +++ b/src/app/api/events/[id]/route.ts @@ -1,11 +1,10 @@ -import { afficherTypesDesAttributs } from "@/app/utils/afficherTypesDesAttributs"; import { handleException } from "@/app/utils/errorHandlerUtils"; import { getEventByIdWithUserEvents, registerOrModifyEvent, removeEvent, } from "@/services/eventService"; -import { eventWithoutId } from "@/types/event"; +import { EventWithoutId } from "@/types/event"; import { eventBodySchema } from "@/validators/api/eventSchema"; import { NextRequest, NextResponse } from "next/server"; @@ -40,13 +39,10 @@ export async function PUT( ) { try { const id: number = Number(params.id); - let body = await request.json(); - let bodytarget: object; - afficherTypesDesAttributs(body.event, EventWithoutId); + const body = await request.json(); - const eventParsed = eventBodySchema.parse(body); - - const event: eventWithoutId = eventParsed.event; + // Parse the body with zod to get the event + const event: EventWithoutId = eventBodySchema.parse(body).event; const result = await registerOrModifyEvent(id, event); return NextResponse.json({ data: result }, { status: 200 }); diff --git a/src/app/api/events/[id]/user/[userId]/step/[stepId]/route.ts b/src/app/api/events/[id]/user/[userId]/step/[stepId]/route.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/step/[stepId]/route.ts b/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/step/[stepId]/route.ts new file mode 100644 index 0000000..90fcc8c --- /dev/null +++ b/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/step/[stepId]/route.ts @@ -0,0 +1,50 @@ +import { handleException } from "@/app/utils/errorHandlerUtils"; +import { + completeEventUserStep, + registerEventUserStep, +} from "@/services/eventService"; +import { NextRequest, NextResponse } from "next/server"; + +/** + * @params request: NextRequest + * @params params: { params: { eventId: string; userId: string; stepId: string } } + * @returns NextResponse + * @description Handles POST request to create a new EventUserStep. + */ +export async function POST( + request: NextRequest, + { params }: { params: { eventId: string; userId: string; stepId: string } } +) { + try { + const eventId: number = Number(params.eventId); + const userId: number = Number(params.userId); + const stepId: number = Number(params.stepId); + + const result = await registerEventUserStep(userId, eventId, stepId); + return NextResponse.json({ data: result }, { status: 200 }); + } catch (error: any) { + return handleException(error); + } +} + +/** + * @params request: NextRequest + * @params params: { eventId: string; userId: string; stepId: string } + * @returns NextResponse + * @description Handles PUT request to mark a step as completed for a user in a specific event. + */ +export async function PUT( + request: NextRequest, + { params }: { params: { eventId: string; userId: string; stepId: string } } +) { + try { + const eventId: number = Number(params.eventId); + const userId: number = Number(params.userId); + const stepId: number = Number(params.stepId); + + const result = await completeEventUserStep(userId, eventId, stepId); + return NextResponse.json({ data: result }, { status: 200 }); + } catch (error: any) { + return handleException(error); + } +} diff --git a/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/steps/route.ts b/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/steps/route.ts new file mode 100644 index 0000000..2f2e84d --- /dev/null +++ b/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/steps/route.ts @@ -0,0 +1,27 @@ +import { handleException } from "@/app/utils/errorHandlerUtils"; +import { getEventUserStepsByUserIdAndEventId } from "@/services/eventService"; +import { NextRequest, NextResponse } from "next/server"; + +/** + * @params request: NextRequest + * @params params: { eventId: string; userId: string } + * @returns NextResponse + * @description Handles GET request to retrieve all EventUserSteps for a user in a specific event. + */ +export async function GET( + request: NextRequest, + { params }: { params: { eventId: string; userId: string } } +) { + try { + const eventId: number = Number(params.eventId); + const userId: number = Number(params.userId); + + console.log("eventId", eventId); + console.log("userId", userId); + + const result = await getEventUserStepsByUserIdAndEventId(userId, eventId); + return NextResponse.json({ data: result }, { status: 200 }); + } catch (error: any) { + return handleException(error); + } +} diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index f2274a3..f1bce45 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -1,8 +1,7 @@ import { handleException } from "@/app/utils/errorHandlerUtils"; import { getAllEvents, registerOrModifyEvent } from "@/services/eventService"; -import { eventWithoutId } from "@/types/event"; +import { EventWithoutId } from "@/types/event"; import { eventBodySchema } from "@/validators/api/eventSchema"; -import { Event } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; /** @@ -26,11 +25,10 @@ export async function GET() { export async function POST(request: NextRequest) { try { const body = await request.json(); - body.event.startAt = new Date(body.event.startAt); - body.event.endAt = new Date(body.event.endAt); - const eventParsed = eventBodySchema.parse(body); - const event: eventWithoutId = eventParsed.event; + // Parse the body with zod to get the event + const event: EventWithoutId = eventBodySchema.parse(body).event; + const result = await registerOrModifyEvent(null, event); return NextResponse.json({ data: result }, { status: 200 }); } catch (error: any) { diff --git a/src/app/api/journeys/[id]/route.ts b/src/app/api/journeys/[id]/route.ts index ac8fb09..ef356c4 100644 --- a/src/app/api/journeys/[id]/route.ts +++ b/src/app/api/journeys/[id]/route.ts @@ -3,7 +3,9 @@ import { registerOrModifyJourney, removeJourney, } from "@/services/journeyService"; -import { Journey, Step } from "@prisma/client"; +import { JourneyWithoutDates } from "@/types/journey"; +import { StepWithoutDates } from "@/types/step"; +import { journeyBodySchema } from "@/validators/api/journeySchema"; import { NextRequest, NextResponse } from "next/server"; /** @@ -19,8 +21,10 @@ export async function PUT( try { const id: number = Number(params.id); const body = await request.json(); - const journey: Journey = body.journey; - const steps: Step[] = body.steps; + // Parse the body with zod to get the journey and steps + const parsedBody = journeyBodySchema.parse(body); + const journey: JourneyWithoutDates = parsedBody.journey; + const steps: StepWithoutDates[] = parsedBody.steps; const result = await registerOrModifyJourney(id, journey, steps); return NextResponse.json({ data: result }, { status: 200 }); diff --git a/src/app/api/journeys/route.ts b/src/app/api/journeys/route.ts index f085ec4..aef1764 100644 --- a/src/app/api/journeys/route.ts +++ b/src/app/api/journeys/route.ts @@ -3,7 +3,9 @@ import { getAllJourneys, registerOrModifyJourney, } from "@/services/journeyService"; -import { Journey, Step } from "@prisma/client"; +import { JourneyWithoutDates } from "@/types/journey"; +import { StepWithoutDates } from "@/types/step"; +import { journeyBodySchema } from "@/validators/api/journeySchema"; import { NextRequest, NextResponse } from "next/server"; /** @@ -27,8 +29,10 @@ export async function GET() { export async function POST(request: NextRequest) { try { const body = await request.json(); - const journey: Journey = body.journey; - const steps: Step[] = body.steps; + // Parse the body with zod to get the journey and steps + const parsedBody = journeyBodySchema.parse(body); + const journey: JourneyWithoutDates = parsedBody.journey; + const steps: StepWithoutDates[] = parsedBody.steps; const result = await registerOrModifyJourney(null, journey, steps); return NextResponse.json({ data: result }, { status: 201 }); diff --git a/src/app/api/steps/[id]/route.ts b/src/app/api/steps/[id]/route.ts index 7950799..e2ecc30 100644 --- a/src/app/api/steps/[id]/route.ts +++ b/src/app/api/steps/[id]/route.ts @@ -4,7 +4,8 @@ import { registerOrModifyStep, removeStep, } from "@/services/stepService"; -import { Step } from "@prisma/client"; +import { StepWithoutDates } from "@/types/step"; +import { stepBodySchema } from "@/validators/api/stepSchema"; import { NextRequest, NextResponse } from "next/server"; /** @@ -39,7 +40,8 @@ export async function PUT( try { const id: number = Number(params.id); const body = await request.json(); - const step: Step = body.step; + // Parse the body with zod to get the step + const step: StepWithoutDates = stepBodySchema.parse(body).step; const result = await registerOrModifyStep(id, step); return NextResponse.json({ data: result }, { status: 200 }); diff --git a/src/app/api/steps/route.ts b/src/app/api/steps/route.ts index 11e73dd..91c407e 100644 --- a/src/app/api/steps/route.ts +++ b/src/app/api/steps/route.ts @@ -1,5 +1,7 @@ import { handleException } from "@/app/utils/errorHandlerUtils"; import { registerOrModifyStep } from "@/services/stepService"; +import { StepWithoutDates } from "@/types/step"; +import { stepBodySchema } from "@/validators/api/stepSchema"; import { Step } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; @@ -10,8 +12,11 @@ import { NextRequest, NextResponse } from "next/server"; */ export async function POST(request: NextRequest) { try { - const stepData: Step = await request.json(); - const result = await registerOrModifyStep(null, stepData); + const body = await request.json(); + // Parse the body with zod to get the step + const step: StepWithoutDates = stepBodySchema.parse(body).step; + + const result = await registerOrModifyStep(null, step); return NextResponse.json({ data: result }, { status: 201 }); } catch (error: any) { return handleException(error); diff --git a/src/app/utils/afficherTypesDesAttributs.ts b/src/app/utils/afficherTypesDesAttributs.ts deleted file mode 100644 index b5cbc69..0000000 --- a/src/app/utils/afficherTypesDesAttributs.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Utilitaire pour obtenir les types des attributs de T -type TypeDesAttributs = { - [K in keyof T]: T[K]; -}; - -// Fonction pour afficher les types des attributs de T -export function afficherTypesDesAttributs( - body: T, - ctor: new () => T -) { - console.log("AFFICHER TYPES DES ATTRIBUTS"); - - // Instancier un nouvel objet de type T - const bodyTypeTarget = new ctor(); - - // Obtenir les clés de l'objet (y compris les symboles) - const keys = [ - ...Object.keys(bodyTypeTarget), - ...Object.getOwnPropertySymbols(bodyTypeTarget), - ] as (keyof T)[]; - - // Créer un objet pour stocker les types des attributs - const types: Record = {}; - - // Parcourir les clés et obtenir les types - keys.forEach((key) => { - const keyAsString = String(key); // Convertir la clé en chaîne de caractères - const valeur = body[key as keyof T]; - types[keyAsString] = typeof valeur; - console.log(`Clé: ${keyAsString}, Type: ${typeof valeur}`); - }); - - // Retourner les types des attributs - return types; -} diff --git a/src/app/utils/errorHandlerUtils.ts b/src/app/utils/errorHandlerUtils.ts index 1f7e135..2819eca 100644 --- a/src/app/utils/errorHandlerUtils.ts +++ b/src/app/utils/errorHandlerUtils.ts @@ -58,11 +58,10 @@ function formatZodErrors(error: ZodError): string { switch (e.code) { case ZodIssueCode.invalid_type: const invalidTypeIssue = e as ZodInvalidTypeIssue; - message += ` (expected ${invalidTypeIssue.expected}, received ${invalidTypeIssue.received})`; break; case ZodIssueCode.invalid_literal: const invalidLiteralIssue = e as ZodInvalidLiteralIssue; - message += ` (expected ${invalidLiteralIssue.expected}, received ${invalidLiteralIssue.received})`; + message += ` (expected ${invalidLiteralIssue.expected}, received** ${invalidLiteralIssue.received})`; break; case ZodIssueCode.too_small: const tooSmallIssue = e as ZodTooSmallIssue; @@ -76,7 +75,7 @@ function formatZodErrors(error: ZodError): string { const enumIssue = e as ZodInvalidEnumValueIssue; message += ` (expected one of: ${enumIssue.options.join( ", " - )}, received: ${enumIssue.received})`; + )}, received**: ${enumIssue.received})`; break; case ZodIssueCode.unrecognized_keys: const keysIssue = e as ZodUnrecognizedKeysIssue; diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts new file mode 100644 index 0000000..f60f3ac --- /dev/null +++ b/src/app/utils/utils.ts @@ -0,0 +1,41 @@ +/** + * @param milliseconds: number + * @param separator: TimeSeparator + * @returns string + * @description Converts a duration from milliseconds to a formatted string with the specified separator. + * @example convertMillisecondsToHoursMinutes(7384000, TimeSeparator.HourMin) // "2h03min" + */ +function convertMillisecondsToHoursMinutes( + milliseconds: number, + separator: TimeSeparator = TimeSeparator.HourMin +): string { + if (typeof milliseconds !== "number" || milliseconds < 0) + throw new Error( + "Invalid input: milliseconds must be a non-negative number." + ); + + const totalMinutes = Math.floor(milliseconds / 60000); + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + let formattedDuration; + const paddedMinutes = String(minutes).padStart(2, "0"); // Ajout du zéro devant les minutes de 0 à 9 + + switch (separator) { + case TimeSeparator.HourMin: + formattedDuration = `${hours}h${paddedMinutes}min`; + break; + case TimeSeparator.Hour: + formattedDuration = `${hours}h${paddedMinutes}`; + break; + case TimeSeparator.Colon: + formattedDuration = `${String(hours).padStart(2, "0")}:${paddedMinutes}`; + break; + default: + throw new Error( + "Invalid separator: Use TimeSeparator.HourMin, TimeSeparator.Hour, or TimeSeparator.Colon" + ); + } + + return formattedDuration; +} diff --git a/src/repositories/eventRepository.ts b/src/repositories/eventRepository.ts index b1b3392..40194cf 100644 --- a/src/repositories/eventRepository.ts +++ b/src/repositories/eventRepository.ts @@ -1,5 +1,5 @@ import prisma from "@/lib/prisma"; -import { eventWithUserEvents, eventWithoutId } from "@/types/event"; +import { EventWithUserEvents, EventWithoutId } from "@/types/event"; import { Event } from "@prisma/client"; /** @@ -7,7 +7,7 @@ import { Event } from "@prisma/client"; * @returns Event * @description Creates a new event with the provided data. */ -export const createEvent = async (data: eventWithoutId): Promise => { +export const createEvent = async (data: EventWithoutId): Promise => { return await prisma.event.create({ data, }); @@ -15,12 +15,12 @@ export const createEvent = async (data: eventWithoutId): Promise => { /** * @params id: number - * @returns eventWithUserEvents | null + * @returns EventWithUserEvents | null * @description Retrieves an event by its id along with associated user events. */ export const readEvent = async ( id: number -): Promise => { +): Promise => { return await prisma.event.findUnique({ where: { id }, include: { @@ -45,7 +45,7 @@ export const readEvents = async (): Promise => { */ export const updateEvent = async ( id: number, - data: eventWithoutId + data: EventWithoutId ): Promise => { return await prisma.event.update({ where: { id }, diff --git a/src/repositories/eventUserStepuserStepRepository.ts b/src/repositories/eventUserStepuserStepRepository.ts new file mode 100644 index 0000000..f7bfcab --- /dev/null +++ b/src/repositories/eventUserStepuserStepRepository.ts @@ -0,0 +1,94 @@ +import prisma from "@/lib/prisma"; +import { EventUserStepWithoutId } from "@/types/eventUserStep"; +import { EventUserStep } from "@prisma/client"; + +/** + * @params data: EventUserStepWithoutId + * @returns EventUserStep + * @description Creates a new user step event with the provided data. + */ +export const createEventUserStep = async ( + data: EventUserStepWithoutId +): Promise => { + return await prisma.eventUserStep.create({ + data, + }); +}; + +/** + * @params id: number + * @returns EventUserStep | null + * @description Retrieves a user step event by its id. + */ +export const readEventUserStep = async ( + id: number +): Promise => { + return await prisma.eventUserStep.findUnique({ where: { id } }); +}; + +/** + * @params userId: number + * @params eventId: number + * @params stepId: number + * @returns EventUserStep | null + * @description Retrieves a user step event by user id, event id, and step id. + */ +export const readEventUserStepByIds = async ( + userId: number, + eventId: number, + stepId: number +): Promise => { + return await prisma.eventUserStep.findFirst({ + where: { userId, eventId, stepId }, + }); +}; + +/** + * @returns EventUserStep[] + * @description Retrieves all user step events. + */ +export const readEventUserSteps = async (): Promise => { + return await prisma.eventUserStep.findMany(); +}; + +/** + * @params userId: number + * @params eventId: number + * @returns EventUserStep[] + * @description Retrieves all user step events for a given user and event. + */ +export const readEventUserStepsByUserIdAndEventId = async ( + userId: number, + eventId: number +): Promise => { + return await prisma.eventUserStep.findMany({ + where: { userId, eventId }, + }); +}; + +/** + * @params id: number + * @params data: EventUserStep + * @returns EventUserStep | null + * @description Updates a user step event with the provided data. + */ +export const updateEventUserStep = async ( + id: number, + data: EventUserStep +): Promise => { + return await prisma.eventUserStep.update({ + where: { id }, + data, + }); +}; + +/** + * @params id: number + * @returns EventUserStep | null + * @description Deletes a user step event by its id. + */ +export const deleteEventUserStep = async ( + id: number +): Promise => { + return await prisma.eventUserStep.delete({ where: { id } }); +}; diff --git a/src/repositories/journeyRepository.ts b/src/repositories/journeyRepository.ts index a9d98bc..fe3f376 100644 --- a/src/repositories/journeyRepository.ts +++ b/src/repositories/journeyRepository.ts @@ -1,18 +1,22 @@ import prisma from "@/lib/prisma"; +import { + JourneyWithoutDates, + JourneyWithComments, + JourneyWithSteps, +} from "@/types/journey"; +import { StepWithoutDates } from "@/types/step"; import { Journey, Step } from "@prisma/client"; -import { journeyWithSteps } from "@/types/journeyWithSteps"; -import { journeyWithComments } from "@/types/journeyWithComments"; /** * @params journey: Journey * @params steps: Step[] - * @returns journeyWithSteps | null + * @returns JourneyWithSteps | null * @description Creates a new journey with the provided data and associated steps. */ export const createJourney = async ( - journey: Journey, - steps: Step[] -): Promise => { + journey: JourneyWithoutDates, + steps: StepWithoutDates[] +): Promise => { return await prisma.journey.create({ include: { steps: { @@ -70,12 +74,12 @@ export const readJourney = async (id: number): Promise => { /** * @params id: number - * @returns journeyWithSteps | null + * @returns JourneyWithSteps | null * @description Retrieves a journey by its id along with associated steps. */ export const readJourneyWithSteps = async ( id: number -): Promise => { +): Promise => { return await prisma.journey.findUnique({ where: { id }, include: { @@ -90,12 +94,12 @@ export const readJourneyWithSteps = async ( /** * @params id: number - * @returns journeyWithComments | null + * @returns JourneyWithComments | null * @description Retrieves a journey by its id along with associated comments. */ export const readJourneyWithComments = async ( id: number -): Promise => { +): Promise => { return await prisma.journey.findUnique({ where: { id }, include: { @@ -120,60 +124,80 @@ export const readJourneys = async (): Promise => { * @params id: number * @params journey: Journey * @params steps: Step[] - * @returns journeyWithSteps | null + * @returns JourneyWithSteps | null * @description Updates a journey with the provided data and associated steps. */ export const updateJourney = async ( id: number, - journey: Journey, - steps: Step[] -): Promise => { - return await prisma.journey.update({ - where: { - id: id, - }, - include: { - steps: { - orderBy: { - stepNumber: "asc", - }, + journey: JourneyWithoutDates, + steps: StepWithoutDates[] +): Promise => { + return await prisma.$transaction(async (prisma) => { + // Update the journey + const updatedJourney = await prisma.journey.update({ + where: { + id: id, }, - }, - data: { - authorId: journey.authorId, - title: journey.title, - description: journey.description, - requirement: journey.requirement, - treasure: journey.treasure, - estimatedDistance: journey.estimatedDistance, - estimatedDuration: journey.estimatedDuration, - cluesDifficulty: journey.cluesDifficulty, - physicalDifficulty: journey.physicalDifficulty, - lastCompletion: journey.lastCompletion, - mobilityImpaired: journey.mobilityImpaired, - partiallySighted: journey.partiallySighted, - partiallyDeaf: journey.partiallyDeaf, - cognitivelyImpaired: journey.cognitivelyImpaired, - steps: { - createMany: { - data: steps.map((step) => ({ - journeyId: journey.id, - puzzle: step.puzzle, - answer: step.answer, - hint: step.hint, - picturePuzzle: step.picturePuzzle, - pictureHint: step.pictureHint, - latitude: step.latitude, - longitude: step.longitude, - address: step.address, - city: step.city, - postalCode: step.postalCode, - country: step.country, - stepNumber: step.stepNumber, - })), - }, + data: { + authorId: journey.authorId, + title: journey.title, + description: journey.description, + requirement: journey.requirement, + treasure: journey.treasure, + estimatedDistance: journey.estimatedDistance, + estimatedDuration: journey.estimatedDuration, + cluesDifficulty: journey.cluesDifficulty, + physicalDifficulty: journey.physicalDifficulty, + lastCompletion: journey.lastCompletion, + mobilityImpaired: journey.mobilityImpaired, + partiallySighted: journey.partiallySighted, + partiallyDeaf: journey.partiallyDeaf, + cognitivelyImpaired: journey.cognitivelyImpaired, }, - }, + }); + + // Upsert steps + for (const step of steps) { + await prisma.step.upsert({ + where: { + id: step.id ? step.id : 0, // Use step.id for existing steps + }, + create: { + journeyId: id, + puzzle: step.puzzle, + answer: step.answer, + hint: step.hint, + picturePuzzle: step.picturePuzzle, + pictureHint: step.pictureHint, + latitude: step.latitude, + longitude: step.longitude, + address: step.address, + city: step.city, + postalCode: step.postalCode, + country: step.country, + stepNumber: step.stepNumber, + }, + update: { + puzzle: step.puzzle, + answer: step.answer, + hint: step.hint, + picturePuzzle: step.picturePuzzle, + pictureHint: step.pictureHint, + latitude: step.latitude, + longitude: step.longitude, + address: step.address, + city: step.city, + postalCode: step.postalCode, + country: step.country, + stepNumber: step.stepNumber, + }, + }); + } + + return await prisma.journey.findUnique({ + where: { id }, + include: { steps: true }, + }); }); }; diff --git a/src/repositories/stepRepository.ts b/src/repositories/stepRepository.ts index eadb45d..bcf56da 100644 --- a/src/repositories/stepRepository.ts +++ b/src/repositories/stepRepository.ts @@ -1,4 +1,5 @@ import prisma from "@/lib/prisma"; +import { StepWithoutDates } from "@/types/step"; import { Step } from "@prisma/client"; /** @@ -6,7 +7,7 @@ import { Step } from "@prisma/client"; * @returns Step * @description Creates a new step with the provided data. */ -export const createStep = async (data: Step): Promise => { +export const createStep = async (data: StepWithoutDates): Promise => { return await prisma.step.create({ data, }); @@ -40,7 +41,7 @@ export const readStepsByJourneyId = async ( */ export const updateStep = async ( id: number, - data: Step + data: StepWithoutDates ): Promise => { return await prisma.step.update({ where: { id }, diff --git a/src/repositories/userEventRepository.ts b/src/repositories/userEventRepository.ts index db3165a..bca3d7e 100644 --- a/src/repositories/userEventRepository.ts +++ b/src/repositories/userEventRepository.ts @@ -1,5 +1,5 @@ import prisma from "@/lib/prisma"; -import { UserEventWithoutId } from "@/types/userEventNullableId"; +import { UserEventWithoutId } from "@/types/userEvent"; import { UserEvent } from "@prisma/client"; /** diff --git a/src/services/commentService.ts b/src/services/commentService.ts index cff8d44..7979a40 100644 --- a/src/services/commentService.ts +++ b/src/services/commentService.ts @@ -10,7 +10,7 @@ import { deleteComment, } from "../repositories/commentRepository"; import { Comment } from "@prisma/client"; -import { CommentWithoutDates } from "@/types/CommentWithoutDates"; +import { CommentWithoutDates } from "@/types/comment"; /** * @params id: number diff --git a/src/services/eventService.ts b/src/services/eventService.ts index 65f4e29..68decd6 100644 --- a/src/services/eventService.ts +++ b/src/services/eventService.ts @@ -10,28 +10,38 @@ import { updateEvent, deleteEvent, } from "../repositories/eventRepository"; -import { Event, UserEvent } from "@prisma/client"; +import { Event, UserEvent, EventUserStep } from "@prisma/client"; import { createUserEvent, deleteUserEvent, readUserEventByUserIdAndEventId, } from "@/repositories/userEventRepository"; -import { eventWithUserEvents, eventWithoutId } from "@/types/event"; -import { UserEventWithoutId } from "@/types/userEventWithoutId"; +import { EventWithUserEvents, EventWithoutId } from "@/types/event"; +import { readUser } from "@/repositories/userRepository"; +import { UserEventWithoutId } from "@/types/userEvent"; +import { readJourneyWithSteps } from "@/repositories/journeyRepository"; +import { JourneyWithSteps } from "@/types/journey"; +import { EventUserStepWithoutId } from "@/types/eventUserStep"; +import { + createEventUserStep, + readEventUserStepByIds, + readEventUserStepsByUserIdAndEventId, + updateEventUserStep, +} from "@/repositories/eventUserStepuserStepRepository"; /** * @params id: number - * @returns eventWithUserEvents | null + * @returns EventWithUserEvents | null * @throws NotFoundException * @description Retrieves an event with its associated user events by its id. */ export const getEventByIdWithUserEvents = async ( id: number -): Promise => { - const eventWithUserEvents: eventWithUserEvents | null = await readEvent(id); - if (!eventWithUserEvents) throw new NotFoundException("Event not found"); +): Promise => { + const EventWithUserEvents: EventWithUserEvents | null = await readEvent(id); + if (!EventWithUserEvents) throw new NotFoundException("Event not found"); - return eventWithUserEvents; + return EventWithUserEvents; }; /** @@ -59,7 +69,7 @@ export const getAllEvents = async (): Promise => { */ export const registerOrModifyEvent = async ( id: number | null, - event: eventWithoutId + event: EventWithoutId ): Promise => { // Check arguments if (id !== null && !Number.isFinite(id)) { @@ -119,20 +129,22 @@ export const joinEvent = async ( userId: number ): Promise => { // Validate arguments - if (!Number.isFinite(userId) || !Number.isFinite(eventId)) { + if (!Number.isFinite(userId) || !Number.isFinite(eventId)) throw new BadRequestException("Invalid userId or eventId"); - } - if (await readUserEventByUserIdAndEventId(userId, eventId)) { - throw new NotFoundException("User already joined event"); - } + if (!(await readUser(userId))) throw new NotFoundException("User not found"); + + if (!(await readEvent(eventId))) + throw new NotFoundException("Event not found"); + + if (await readUserEventByUserIdAndEventId(userId, eventId)) + throw new BadRequestException("User already joined event"); const userEvent: UserEventWithoutId = { userId: userId, eventId: eventId }; const createdUserEvent = await createUserEvent(userEvent); - if (!createdUserEvent) { + if (!createdUserEvent) throw new InternalServerErrorException("Unable to join event"); - } return createdUserEvent; }; @@ -147,8 +159,8 @@ export const joinEvent = async ( * @description Allows a user to leave an event. */ export const leaveEvent = async ( - userId: number, - eventId: number + eventId: number, + userId: number ): Promise => { // Validate arguments if (!Number.isFinite(userId) || !Number.isFinite(eventId)) { @@ -156,16 +168,186 @@ export const leaveEvent = async ( } const userEvent = await readUserEventByUserIdAndEventId(userId, eventId); - if (!userEvent) { - throw new NotFoundException("User not found in event"); - } + if (!userEvent) throw new NotFoundException("UserEvent not found"); // Delete the UserEvent const deletedUserEvent = await deleteUserEvent(userEvent.id); - if (!deletedUserEvent) { + if (!deletedUserEvent) throw new InternalServerErrorException("Unable to leave event"); - } return deletedUserEvent; }; + +/** + * @params userId: number + * @params eventId: number + * @params stepId: number + * @returns EventUserStep | null + * @throws BadRequestException + * @throws NotFoundException + * @throws InternalServerErrorException + * @description Register a EventUserStep that allow to track the user completion of the event + */ +export const registerEventUserStep = async ( + userId: number, + eventId: number, + stepId: number +): Promise => { + if ( + !Number.isFinite(userId) || + !Number.isFinite(eventId) || + !Number.isFinite(stepId) + ) + throw new BadRequestException("Invalid userId, eventId, or stepId"); + + if (!(await readUser(userId))) throw new NotFoundException("User not found"); + + const event = await readEvent(eventId); + if (!event) throw new NotFoundException("Event not found"); + + const journey: JourneyWithSteps | null = await readJourneyWithSteps( + event.journeyId + ); + + if (!journey || !journey.steps.some((step) => step.id === stepId)) + throw new BadRequestException( + "Invalid stepId for the given event's journey" + ); + + const currentStep = journey.steps.find((step) => step.id === stepId); + if (!currentStep) + throw new BadRequestException("Invalid stepId for the journey"); + + if (currentStep.stepNumber !== 1) { + // Check if the previous step is completed + const previousStep = journey.steps.find( + (step) => step.stepNumber === currentStep.stepNumber - 1 + ); + if (!previousStep) + throw new InternalServerErrorException("Internal server error"); + + const previousEventUserStep = await readEventUserStepByIds( + userId, + eventId, + previousStep.id + ); + if (!previousEventUserStep || previousEventUserStep.endAt === null) { + throw new BadRequestException( + "Previous step is not completed, cannot proceed to the next step" + ); + } + } + + if (await readEventUserStepByIds(userId, eventId, stepId)) + throw new BadRequestException("User step event already exists"); + + const EventUserStepData: EventUserStepWithoutId = { + userId, + eventId, + stepId, + startAt: new Date(), // Set by the service + endAt: null, // Intentionally set to null. Will be updated by completeEventUserStep function + durationMs: 0, // completeEventUserStep will calculate the duration + }; + + const EventUserStep = await createEventUserStep(EventUserStepData); + + if (!EventUserStep) + throw new InternalServerErrorException("Internal server error"); + + return EventUserStep; +}; + +/** + * @params userId: number + * @params eventId: number + * @params stepId: number + * @returns EventUserStep | null + * @throws BadRequestException + * @throws NotFoundException + * @throws InternalServerErrorException + * @description Marks a step as completed for a user in a specific event, updating endAt and duration. + */ +export const completeEventUserStep = async ( + userId: number, + eventId: number, + stepId: number +): Promise => { + // Validate arguments + if ( + !Number.isFinite(userId) || + !Number.isFinite(eventId) || + !Number.isFinite(stepId) + ) { + throw new BadRequestException("Invalid userId, eventId, or stepId"); + } + + // Check if the EventUserStep exists + const eventUserStep = await readEventUserStepByIds(userId, eventId, stepId); + if (!eventUserStep) throw new NotFoundException("EventUserStep not found"); + + if (eventUserStep.endAt !== null) + throw new BadRequestException("EventUserStep already completed"); + + // Set endAt to the current date and time + eventUserStep.endAt = new Date(); + + // Calculate the duration in milliseconds + eventUserStep.durationMs = + eventUserStep.endAt.getTime() - new Date(eventUserStep.startAt).getTime(); + + // Update the EventUserStep with endAt and duration + const updatedEventUserStep = await updateEventUserStep( + eventUserStep.id, + eventUserStep + ); + + if (!updatedEventUserStep) { + throw new InternalServerErrorException( + "Unable to complete user step event" + ); + } + + return updatedEventUserStep; +}; + +/** + * @params userId: number + * @params eventId: number + * @returns EventUserStep[] + * @throws BadRequestException + * @throws NotFoundException + * @throws InternalServerErrorException + * @description Retrieves all user step events for a given user and event. + */ +export const getEventUserStepsByUserIdAndEventId = async ( + userId: number, + eventId: number +): Promise => { + // Validate arguments + if (!Number.isFinite(userId) || !Number.isFinite(eventId)) { + throw new BadRequestException("Invalid userId or eventId"); + } + + if (!(await readUser(userId))) { + throw new NotFoundException("User not found"); + } + + if (!(await readEvent(eventId))) { + throw new NotFoundException("Event not found"); + } + + const EventUserSteps = await readEventUserStepsByUserIdAndEventId( + userId, + eventId + ); + + if (!EventUserSteps || EventUserSteps.length === 0) { + throw new NotFoundException( + "EventUserSteps not found for the given user and event" + ); + } + + return EventUserSteps; +}; diff --git a/src/services/journeyService.ts b/src/services/journeyService.ts index ac00774..a3b0a12 100644 --- a/src/services/journeyService.ts +++ b/src/services/journeyService.ts @@ -12,9 +12,15 @@ import { readJourneyWithSteps, readJourneyWithComments, } from "../repositories/journeyRepository"; -import { Journey, Step } from "@prisma/client"; -import { journeyWithSteps } from "@/types/journeyWithSteps"; -import { journeyWithComments } from "@/types/journeyWithComments"; +import { Journey } from "@prisma/client"; +import { + JourneyWithoutDates, + JourneyWithComments, + JourneyWithSteps, +} from "@/types/journey"; +import { StepWithoutDates } from "@/types/step"; +import next from "next"; +import { number } from "zod"; /** * @params id: number @@ -31,35 +37,35 @@ export const getJourneyById = async (id: number): Promise => { /** * @params id: number - * @returns journeyWithSteps | null + * @returns JourneyWithSteps | null * @throws NotFoundException * @description Retrieves a journey by its id with its steps. */ export const getJourneyByIdWithSteps = async ( id: number -): Promise => { - const journeyWithSteps: journeyWithSteps | null = await readJourneyWithSteps( +): Promise => { + const JourneyWithSteps: JourneyWithSteps | null = await readJourneyWithSteps( id ); - if (!journeyWithSteps) throw new NotFoundException("Journey not found"); + if (!JourneyWithSteps) throw new NotFoundException("Journey not found"); - return journeyWithSteps; + return JourneyWithSteps; }; /** * @params id: number - * @returns journeyWithComments | null + * @returns JourneyWithComments | null * @throws NotFoundException * @description Retrieves a journey by its id with its comments. */ export const getJourneyByIdWithComments = async ( id: number -): Promise => { - const journeyWithComments: journeyWithComments | null = +): Promise => { + const JourneyWithComments: JourneyWithComments | null = await readJourneyWithComments(id); - if (!journeyWithComments) throw new NotFoundException("Journey not found"); + if (!JourneyWithComments) throw new NotFoundException("Journey not found"); - return journeyWithComments; + return JourneyWithComments; }; /** @@ -88,8 +94,8 @@ export const getAllJourneys = async (): Promise => { */ export const registerOrModifyJourney = async ( id: number | null, - journey: Journey, - steps: Step[] + journey: JourneyWithoutDates, + steps: StepWithoutDates[] ): Promise => { // Check arguments if (id !== null && !Number.isFinite(id)) { @@ -98,32 +104,52 @@ export const registerOrModifyJourney = async ( if (!journey) throw new BadRequestException("Invalid journey"); if (!steps) throw new BadRequestException("Invalid steps"); - // Check if steps are valid (unique and sequential) - for (let i = 0; i < steps.length; i++) { - const currentStep = steps[i]; - const nextStep = steps[i + 1]; - - if (currentStep.stepNumber !== i + 1) { - throw new BadRequestException("Invalid stepNumber"); - } - - if (nextStep && currentStep.stepNumber >= nextStep.stepNumber) { - throw new BadRequestException("Duplicate or non-sequential stepNumber"); - } - } - - let upsertedJourneyWithSteps: journeyWithSteps | null; + let upsertedJourneyWithSteps: JourneyWithSteps | null; // Check if register or modify if (id === null) { + steps = SortAndValidatetSteps(steps); + upsertedJourneyWithSteps = await createJourney(journey, steps); if (!upsertedJourneyWithSteps) throw new InternalServerErrorException("Internal server error"); } else { - const journeyToUpdate = await readJourney(id); + const journeyToUpdate = await readJourneyWithSteps(id); if (!journeyToUpdate) throw new NotFoundException("Journey not found"); - upsertedJourneyWithSteps = await updateJourney(id, journey, steps); + // Check if the provided step ids exist for the journey + const existingStepIds = journeyToUpdate.steps.map((step) => step.id); + + const providedStepIds = steps + .filter((step) => Number.isFinite(step.id)) + .map((step) => step.id as number); + + for (const stepId of providedStepIds) { + if (!existingStepIds.includes(stepId)) { + throw new BadRequestException( + `Step with id ${stepId} does not exist for the journey` + ); + } + } + + // Combine existing steps and provided steps + const combinedSteps = journeyToUpdate.steps.map((existingStep) => { + const updatedStep = steps.find((step) => step.id === existingStep.id); + return updatedStep ? updatedStep : existingStep; + }); + + // Add new steps that do not have an id + const newSteps = steps.filter((step) => !step.id); + combinedSteps.push(...newSteps); + + // Validate and sort the combined steps + const sortedAndValidatedSteps = SortAndValidatetSteps(combinedSteps); + + upsertedJourneyWithSteps = await updateJourney( + id, + journey, + sortedAndValidatedSteps + ); if (!upsertedJourneyWithSteps) throw new InternalServerErrorException("Internal server error"); } @@ -149,3 +175,23 @@ export const removeJourney = async (id: number): Promise => { return deletedJourney; }; + +export const SortAndValidatetSteps = ( + steps: StepWithoutDates[] +): StepWithoutDates[] => { + // Sort steps by stepNumber + steps.sort((a, b) => a.stepNumber - b.stepNumber); + + // Check if stepNumbers are unique and sequential + for (let i = 0; i < steps.length; i++) { + if (steps[i].stepNumber !== i + 1) { + let errStr: String = "Sorted list Steps : "; + for (const step of steps) { + errStr += " id:" + step.id + "-stepNumber:" + step.stepNumber + " ||"; + } + throw new BadRequestException("Invalid stepNumber sequence. " + errStr); + } + } + + return steps; +}; diff --git a/src/services/stepService.ts b/src/services/stepService.ts index 48c4129..6147829 100644 --- a/src/services/stepService.ts +++ b/src/services/stepService.ts @@ -12,6 +12,7 @@ import { } from "../repositories/stepRepository"; import { Step } from "@prisma/client"; import { readJourneyWithSteps } from "@/repositories/journeyRepository"; +import { StepWithoutDates } from "@/types/step"; /** * @params journeyId: number @@ -32,7 +33,7 @@ export const getStepsByJourneyID = async ( /** * @params id: number | null - * @params step: Step + * @params step: StepWithoutDates * @returns Step | null * @throws BadRequestException * @throws NotFoundException @@ -41,7 +42,7 @@ export const getStepsByJourneyID = async ( */ export const registerOrModifyStep = async ( id: number | null, - step: Step + step: StepWithoutDates ): Promise => { // Check arguments if (id !== null && !Number.isFinite(id)) { @@ -50,36 +51,41 @@ export const registerOrModifyStep = async ( if (!step) throw new BadRequestException("Invalid step"); - // Check if journey exists and stepNumber is unique const journey = await readJourneyWithSteps(step.journeyId); - if (!journey) throw new NotFoundException("Journey not found"); - // Check if stepNumber is unique and sequential - const lastStep = journey.steps[journey.steps.length - 1]; - if (lastStep && step.stepNumber !== lastStep.stepNumber + 1) { - throw new BadRequestException("Invalid step number"); - } - let upsertedStep: Step | null; - // Check if register or modify if (id === null) { + // Creating a new step + await validateStepNumberForCreation(journey.steps, step); + upsertedStep = await createStep(step); - if (!upsertedStep) + if (!upsertedStep) { throw new InternalServerErrorException("Internal server error"); + } } else { - const stepToUpdate = await readStep(id); + // Updating an existing step + const stepToUpdate = journey.steps.find((s) => s.id === id); + if (!stepToUpdate) throw new NotFoundException("Step not found"); + // Check if stepNumber is being modified + if (step.stepNumber !== stepToUpdate.stepNumber) { + throw new BadRequestException( + "Modification of stepNumber is not allowed." + ); + } + + // Proceed with the update if stepNumber is not modified upsertedStep = await updateStep(id, step); - if (!upsertedStep) + if (!upsertedStep) { throw new InternalServerErrorException("Internal server error"); + } } return upsertedStep; }; - /** * @params id: number * @returns Step | null @@ -115,8 +121,8 @@ export const removeStep = async (id: number): Promise => { // Update step numbers of subsequent steps if (journey) { const stepsToUpdate = journey.steps - .filter((s) => s.stepNumber > step.stepNumber) - .sort((a, b) => a.stepNumber - b.stepNumber); // Sort stepsToUpdate in ascending order based on stepNumber + .filter((s: Step) => s.stepNumber > step.stepNumber) + .sort((a: Step, b: Step) => a.stepNumber - b.stepNumber); // Sort stepsToUpdate in ascending order based on stepNumber for (const s of stepsToUpdate) { s.stepNumber -= 1; const updatedStep = await updateStep(s.id, s); @@ -127,3 +133,23 @@ export const removeStep = async (id: number): Promise => { return deletedStep; }; + +async function validateStepNumberForCreation( + steps: Step[], + newStep: StepWithoutDates +) { + const stepCount = steps.length; + + if (stepCount === 0 && newStep.stepNumber !== 1) { + throw new BadRequestException("The first step must have step number 1."); + } + + if (stepCount > 0) { + const lastStep = steps[stepCount - 1]; + if (newStep.stepNumber !== lastStep.stepNumber + 1) { + throw new BadRequestException( + `Invalid step number. Must be sequential. Previous step number: ${lastStep.stepNumber}` + ); + } + } +} diff --git a/src/types/CommentWithoutDates.ts b/src/types/comment.ts similarity index 100% rename from src/types/CommentWithoutDates.ts rename to src/types/comment.ts diff --git a/src/types/enums/timeSeparator.ts b/src/types/enums/timeSeparator.ts new file mode 100644 index 0000000..e806300 --- /dev/null +++ b/src/types/enums/timeSeparator.ts @@ -0,0 +1,5 @@ +enum TimeSeparator { + HourMin = " ", + Hour = "h", + Colon = ":", +} diff --git a/src/types/event.ts b/src/types/event.ts index 4d68dbb..56f0c6a 100644 --- a/src/types/event.ts +++ b/src/types/event.ts @@ -1,10 +1,10 @@ import { Prisma } from "@prisma/client"; -export type eventWithUserEvents = Prisma.EventGetPayload<{ +export type EventWithUserEvents = Prisma.EventGetPayload<{ include: { userEvents: true }; }>; -export type eventWithoutId = { +export type EventWithoutId = { authorId: number; journeyId: number; title: string; diff --git a/src/types/eventUserStep.ts b/src/types/eventUserStep.ts new file mode 100644 index 0000000..be96704 --- /dev/null +++ b/src/types/eventUserStep.ts @@ -0,0 +1,8 @@ +export type EventUserStepWithoutId = { + userId: number; + stepId: number; + eventId: number; + startAt: Date | undefined; + endAt: Date | null; + durationMs: number | null; +}; diff --git a/src/types/journey.ts b/src/types/journey.ts index 849f4c7..37d7d33 100644 --- a/src/types/journey.ts +++ b/src/types/journey.ts @@ -1,9 +1,27 @@ import { Prisma } from "@prisma/client"; -export type journeyWithComments = Prisma.JourneyGetPayload<{ +export type JourneyWithComments = Prisma.JourneyGetPayload<{ include: { comments: true }; }>; -export type journeyWithSteps = Prisma.JourneyGetPayload<{ +export type JourneyWithSteps = Prisma.JourneyGetPayload<{ include: { steps: true }; }>; + +export type JourneyWithoutDates = { + id?: number; + authorId: number; + title: string; + description: string; + requirement: string; + treasure: string; + estimatedDistance: number; + estimatedDuration?: number; + cluesDifficulty: number; + physicalDifficulty: number; + lastCompletion?: Date; + mobilityImpaired: string; + partiallySighted: string; + partiallyDeaf: string; + cognitivelyImpaired: string; +}; diff --git a/src/types/step.ts b/src/types/step.ts new file mode 100644 index 0000000..2794185 --- /dev/null +++ b/src/types/step.ts @@ -0,0 +1,16 @@ +export type StepWithoutDates = { + id?: number; + journeyId: number; + puzzle: string; + answer: string; + hint: string; + picturePuzzle?: string | null; + pictureHint?: string | null; + latitude: number; + longitude: number; + address?: string | null; + city?: string | null; + postalCode?: string | null; + country?: string | null; + stepNumber: number; +}; diff --git a/src/types/userEventWithoutId.ts b/src/types/userEvent.ts similarity index 100% rename from src/types/userEventWithoutId.ts rename to src/types/userEvent.ts diff --git a/src/validators/api/commentSchema.ts b/src/validators/api/commentSchema.ts index e6e85c7..409bf17 100644 --- a/src/validators/api/commentSchema.ts +++ b/src/validators/api/commentSchema.ts @@ -1,14 +1,18 @@ import { z } from "zod"; -// Comment schema -export const commentSchema = z.object({ - authorId: z.number().int(), - content: z.string(), - rating: z.number().int().nullable(), - journeyId: z.number().int(), +// Comment schema with custom error messages +const baseCommentSchema = z.object({ + authorId: z.number({ required_error: "Required field" }).int(), + content: z + .string({ required_error: "Required field" }) + .max(500, { message: "Please enter less than 500 characters" }), + rating: z + .number({ required_error: "Required field" }) + .int({ message: "Please provide an integer" }), + journeyId: z.number({ required_error: "Required field" }).int(), }); // Combined schema for the body export const commentBodySchema = z.object({ - comment: commentSchema, + comment: baseCommentSchema, }); diff --git a/src/validators/api/eventSchema.ts b/src/validators/api/eventSchema.ts index e481c8f..4705252 100644 --- a/src/validators/api/eventSchema.ts +++ b/src/validators/api/eventSchema.ts @@ -1,28 +1,34 @@ import { z } from "zod"; -// Event schema de base avec des messages d'erreur personnalisés +// Event schema with custom error messages const baseEventSchema = z.object({ - authorId: z.number({ required_error: "Ce champ est requis" }).int(), - journeyId: z.number({ required_error: "Ce champ est requis" }).int(), + authorId: z.number({ required_error: "Required field" }).int(), + journeyId: z.number({ required_error: "Required field" }).int(), title: z - .string({ required_error: "Ce champ est requis" }) - .max(255, { message: "Veuillez renseigner moins de 255 caractères" }), + .string({ required_error: "Required field" }) + .max(255, { message: "Please enter less than 255 characters" }), image: z - .string({ required_error: "Ce champ est requis" }) - .url({ message: "Veuillez fournir une URL valide" }), + .string({ required_error: "Required field" }) + .url({ message: "Please provide a valid URL" }), numberPlayerMin: z - .number({ required_error: "Ce champ est requis" }) - .int({ message: "Veuillez fournir un nombre entier" }), + .number({ required_error: "Required field" }) + .int({ message: "Please provide an integer" }), numberPlayerMax: z - .number({ required_error: "Ce champ est requis" }) - .int({ message: "Veuillez fournir un nombre entier" }), + .number({ required_error: "Required field" }) + .int({ message: "Please provide an integer" }), description: z - .string({ required_error: "Ce champ est requis" }) - .max(500, { message: "Veuillez renseigner moins de 500 caractères" }), - isPrivate: z.boolean({ required_error: "Ce champ est requis" }), + .string({ required_error: "Required field" }) + .max(500, { message: "Please enter less than 500 characters" }), + isPrivate: z.boolean({ required_error: "Required field" }), accessCode: z.string().optional(), - startAt: z.date({ required_error: "Ce champ est requis" }), - endAt: z.date({ required_error: "Ce champ est requis" }), + startAt: z.preprocess( + (arg) => (typeof arg === "string" ? new Date(arg) : arg), + z.date({ required_error: "Required field" }) + ), + endAt: z.preprocess( + (arg) => (typeof arg === "string" ? new Date(arg) : arg), + z.date({ required_error: "Required field" }) + ), }); export const eventSchema = baseEventSchema.superRefine((data, ctx) => { diff --git a/src/validators/api/journeySchema.ts b/src/validators/api/journeySchema.ts index 56ba7da..12da448 100644 --- a/src/validators/api/journeySchema.ts +++ b/src/validators/api/journeySchema.ts @@ -1,24 +1,67 @@ import { z } from "zod"; -import { stepSchema } from "./stepSchema"; +import { baseStepSchema } from "./stepSchema"; -export const journeySchema = z.object({ - authorId: z.number().int(), - title: z.string(), - description: z.string(), - requirement: z.string(), - treasure: z.string(), - estimatedDistance: z.number().int(), - estimatedDuration: z.number().int().nullable().optional(), - cluesDifficulty: z.number().int(), - physicalDifficulty: z.number().int(), - lastCompletion: z.string().nullable().optional(), // Using z.string() for ISO date strings - mobilityImpaired: z.string(), - partiallySighted: z.string(), - partiallyDeaf: z.string(), - cognitivelyImpaired: z.string(), +// Journey schema with custom error messages +const baseJourneySchema = z.object({ + authorId: z + .number({ required_error: "Required field" }) + .int({ message: "Must be an integer" }), + title: z + .string({ required_error: "Required field" }) + .max(255, { message: "Please enter less than 255 characters" }), + description: z + .string({ required_error: "Required field" }) + .max(500, { message: "Please enter less than 500 characters" }), + requirement: z + .string({ required_error: "Required field" }) + .max(255, { message: "Please enter less than 255 characters" }), + treasure: z + .string({ required_error: "Required field" }) + .max(1000, { message: "Please enter less than 1000 characters" }), + estimatedDistance: z + .number({ required_error: "Required field" }) + .int({ message: "Must be an integer" }), + estimatedDuration: z + .number({ required_error: "Required field" }) + .int({ message: "Must be an integer" }), + cluesDifficulty: z + .number({ required_error: "Required field" }) + .int({ message: "Must be an integer" }), + physicalDifficulty: z + .number({ required_error: "Required field" }) + .int({ message: "Must be an integer" }), + lastCompletion: z.preprocess( + (arg) => (typeof arg === "string" ? new Date(arg) : arg), + z.date({ required_error: "Required field" }) + ), + mobilityImpaired: z + .string({ required_error: "Required field" }) + .max(255, { message: "Please enter less than 255 characters" }), + partiallySighted: z + .string({ required_error: "Required field" }) + .max(255, { message: "Please enter less than 255 characters" }), + partiallyDeaf: z + .string({ required_error: "Required field" }) + .max(255, { message: "Please enter less than 255 characters" }), + cognitivelyImpaired: z + .string({ required_error: "Required field" }) + .max(255, { message: "Please enter less than 255 characters" }), }); -export const journeyWithStepsBodySchema = z.object({ - journey: journeySchema, - steps: z.array(stepSchema), -}); +// Schéma combiné pour le corps de la requête avec transformation des steps pour inclure journeyId +export const journeyBodySchema = z + .object({ + journey: baseJourneySchema, + steps: z.array(baseStepSchema), + }) + .transform((data) => { + const { journey, steps } = data; + + // Ajouter journeyId à chaque étape avant validation + const stepsWithJourneyId = steps.map((step) => ({ + ...step, + journeyId: journey.authorId, + })); + + return { journey, steps: stepsWithJourneyId }; + }); diff --git a/src/validators/api/stepSchema.ts b/src/validators/api/stepSchema.ts index 56567b2..a1a2a62 100644 --- a/src/validators/api/stepSchema.ts +++ b/src/validators/api/stepSchema.ts @@ -1,23 +1,49 @@ import { z } from "zod"; -// Step schema -export const stepSchema = z.object({ - puzzle: z.string(), - answer: z.string(), - hint: z.string(), - picturePuzzle: z.string().nullable().optional(), - pictureHint: z.string().nullable().optional(), - latitude: z.number(), - longitude: z.number(), - address: z.string(), - city: z.string(), - postalCode: z.string(), - country: z.string(), - stepNumber: z.number().int(), - journeyId: z.number().int(), +// Step schema with custom error messages +export const baseStepSchema = z.object({ + id: z + .string() + .optional() + .transform((val) => (val ? Number(val) : undefined)), + puzzle: z + .string({ required_error: "Required field" }) + .max(1000, { message: "Please enter less than 1000 characters" }), + answer: z + .string({ required_error: "Required field" }) + .max(255, { message: "Please enter less than 255 characters" }), + hint: z + .string({ required_error: "Required field" }) + .max(1000, { message: "Please enter less than 1000 characters" }), + picturePuzzle: z + .string({ required_error: "Required field" }) + .url({ message: "Please provide a valid URL" }), + pictureHint: z + .string({ required_error: "Required field" }) + .url({ message: "Please provide a valid URL" }), + latitude: z.number({ required_error: "Required field" }), + longitude: z.number({ required_error: "Required field" }), + address: z + .string() + .max(255, { message: "Please enter less than 255 characters" }) + .optional(), + city: z + .string() + .max(50, { message: "Please enter less than 50 characters" }) + .optional(), + postalCode: z + .string() + .regex(/^\d{5}$/, { message: "Postal code must be exactly 5 digits" }) + .optional(), + country: z + .string() + .max(50, { message: "Please enter less than 50 characters" }) + .optional(), + stepNumber: z.number({ required_error: "Required field" }).int(), + journeyId: z.number({ required_error: "Required field" }).int(), }); // Combined schema for the body export const stepBodySchema = z.object({ - step: stepSchema, + step: baseStepSchema, }); From f890daaa63e4551a4956938f7c9dba793e1680d3 Mon Sep 17 00:00:00 2001 From: Guillaume DEMAN Date: Sun, 23 Jun 2024 20:16:02 +0200 Subject: [PATCH 4/6] Clean console.log --- .../event/[eventId]/user/[userId]/steps/route.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/steps/route.ts b/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/steps/route.ts index 2f2e84d..22107bc 100644 --- a/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/steps/route.ts +++ b/src/app/api/events/eventUserSteps/event/[eventId]/user/[userId]/steps/route.ts @@ -16,9 +16,6 @@ export async function GET( const eventId: number = Number(params.eventId); const userId: number = Number(params.userId); - console.log("eventId", eventId); - console.log("userId", userId); - const result = await getEventUserStepsByUserIdAndEventId(userId, eventId); return NextResponse.json({ data: result }, { status: 200 }); } catch (error: any) { From 98dc6fd1fea361d71a6e14e6be1de99658020223 Mon Sep 17 00:00:00 2001 From: Guillaume DEMAN Date: Sun, 23 Jun 2024 20:21:35 +0200 Subject: [PATCH 5/6] Delete useless folder --- src/app/api/events/[id]/user/[userId]/step/[stepId]/route.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/app/api/events/[id]/user/[userId]/step/[stepId]/route.ts diff --git a/src/app/api/events/[id]/user/[userId]/step/[stepId]/route.ts b/src/app/api/events/[id]/user/[userId]/step/[stepId]/route.ts deleted file mode 100644 index e69de29..0000000 From 77c9fcb447d7cdd9dda5220ab27d03507fd9c48d Mon Sep 17 00:00:00 2001 From: Guillaume DEMAN Date: Sun, 23 Jun 2024 20:28:30 +0200 Subject: [PATCH 6/6] Cleaning code + rename eventUserStepRepository.ts --- src/repositories/commentRepository.ts | 2 +- ...UserStepuserStepRepository.ts => eventUserStepRepository.ts} | 0 src/repositories/journeyRepository.ts | 2 +- src/services/eventService.ts | 2 +- src/services/journeyService.ts | 2 -- 5 files changed, 3 insertions(+), 5 deletions(-) rename src/repositories/{eventUserStepuserStepRepository.ts => eventUserStepRepository.ts} (100%) diff --git a/src/repositories/commentRepository.ts b/src/repositories/commentRepository.ts index 83c4dc3..3f9cb98 100644 --- a/src/repositories/commentRepository.ts +++ b/src/repositories/commentRepository.ts @@ -1,5 +1,5 @@ import prisma from "@/lib/prisma"; -import { CommentWithoutDates } from "@/types/CommentWithoutDates"; +import { CommentWithoutDates } from "@/types/comment"; import { Comment } from "@prisma/client"; /** diff --git a/src/repositories/eventUserStepuserStepRepository.ts b/src/repositories/eventUserStepRepository.ts similarity index 100% rename from src/repositories/eventUserStepuserStepRepository.ts rename to src/repositories/eventUserStepRepository.ts diff --git a/src/repositories/journeyRepository.ts b/src/repositories/journeyRepository.ts index fe3f376..784e383 100644 --- a/src/repositories/journeyRepository.ts +++ b/src/repositories/journeyRepository.ts @@ -5,7 +5,7 @@ import { JourneyWithSteps, } from "@/types/journey"; import { StepWithoutDates } from "@/types/step"; -import { Journey, Step } from "@prisma/client"; +import { Journey } from "@prisma/client"; /** * @params journey: Journey diff --git a/src/services/eventService.ts b/src/services/eventService.ts index 68decd6..1cbb993 100644 --- a/src/services/eventService.ts +++ b/src/services/eventService.ts @@ -27,7 +27,7 @@ import { readEventUserStepByIds, readEventUserStepsByUserIdAndEventId, updateEventUserStep, -} from "@/repositories/eventUserStepuserStepRepository"; +} from "@/repositories/eventUserStepRepository"; /** * @params id: number diff --git a/src/services/journeyService.ts b/src/services/journeyService.ts index a3b0a12..3d1ac54 100644 --- a/src/services/journeyService.ts +++ b/src/services/journeyService.ts @@ -19,8 +19,6 @@ import { JourneyWithSteps, } from "@/types/journey"; import { StepWithoutDates } from "@/types/step"; -import next from "next"; -import { number } from "zod"; /** * @params id: number