diff --git a/apps/api/src/entities/exercise.entity.ts b/apps/api/src/entities/exercise.entity.ts index 07d2831..44b1a3e 100644 --- a/apps/api/src/entities/exercise.entity.ts +++ b/apps/api/src/entities/exercise.entity.ts @@ -4,8 +4,8 @@ import { Video } from './video.entity'; @Entity() export class Exercise { - @PrimaryKey() - id!: number; + @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) + id!: string; @ManyToOne(() => ExerciseType) exerciseType!: ExerciseType; diff --git a/apps/api/src/entities/exerciseType.entity.ts b/apps/api/src/entities/exerciseType.entity.ts index 6da9e17..9acdce7 100644 --- a/apps/api/src/entities/exerciseType.entity.ts +++ b/apps/api/src/entities/exerciseType.entity.ts @@ -9,8 +9,8 @@ import { Exercise } from './exercise.entity'; @Entity() export class ExerciseType { - @PrimaryKey() - id!: number; + @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) + id!: string; @OneToMany( () => Exercise, diff --git a/apps/api/src/entities/test.entity.ts b/apps/api/src/entities/test.entity.ts index 2cb4a5e..d65f702 100644 --- a/apps/api/src/entities/test.entity.ts +++ b/apps/api/src/entities/test.entity.ts @@ -2,8 +2,8 @@ import { Entity, PrimaryKey, Property } from '@mikro-orm/core'; @Entity() export class Test { - @PrimaryKey() - id!: number; + @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) + id!: string; @Property() createdAt = new Date(); diff --git a/apps/api/src/entities/video.entity.ts b/apps/api/src/entities/video.entity.ts index e888d85..1ea2e91 100644 --- a/apps/api/src/entities/video.entity.ts +++ b/apps/api/src/entities/video.entity.ts @@ -8,8 +8,8 @@ interface VideoMetadata { @Entity() export class Video { - @PrimaryKey() - id!: number; + @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) + id!: string; @Property() src!: string; diff --git a/apps/api/src/migrations/.snapshot-dropit.json b/apps/api/src/migrations/.snapshot-dropit.json index 09d0b27..1199450 100644 --- a/apps/api/src/migrations/.snapshot-dropit.json +++ b/apps/api/src/migrations/.snapshot-dropit.json @@ -8,12 +8,13 @@ "columns": { "id": { "name": "id", - "type": "serial", + "type": "uuid", "unsigned": false, - "autoincrement": true, - "primary": true, + "autoincrement": false, + "primary": false, "nullable": false, - "mappedType": "integer" + "default": "gen_random_uuid()", + "mappedType": "uuid" }, "name": { "name": "name", @@ -68,12 +69,13 @@ "columns": { "id": { "name": "id", - "type": "serial", + "type": "uuid", "unsigned": false, - "autoincrement": true, - "primary": true, + "autoincrement": false, + "primary": false, "nullable": false, - "mappedType": "integer" + "default": "gen_random_uuid()", + "mappedType": "uuid" }, "created_at": { "name": "created_at", @@ -128,12 +130,13 @@ "columns": { "id": { "name": "id", - "type": "serial", + "type": "uuid", "unsigned": false, - "autoincrement": true, - "primary": true, + "autoincrement": false, + "primary": false, "nullable": false, - "mappedType": "integer" + "default": "gen_random_uuid()", + "mappedType": "uuid" }, "src": { "name": "src", @@ -198,30 +201,31 @@ "columns": { "id": { "name": "id", - "type": "serial", + "type": "uuid", "unsigned": false, - "autoincrement": true, - "primary": true, + "autoincrement": false, + "primary": false, "nullable": false, - "mappedType": "integer" + "default": "gen_random_uuid()", + "mappedType": "uuid" }, "exercise_type_id": { "name": "exercise_type_id", - "type": "int", + "type": "uuid", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "mappedType": "integer" + "mappedType": "uuid" }, "video_id": { "name": "video_id", - "type": "int", + "type": "uuid", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "mappedType": "integer" + "mappedType": "uuid" }, "name": { "name": "name", diff --git a/apps/api/src/migrations/Migration20250103192244_idNumberToUuid.ts b/apps/api/src/migrations/Migration20250103192244_idNumberToUuid.ts new file mode 100644 index 0000000..6f78442 --- /dev/null +++ b/apps/api/src/migrations/Migration20250103192244_idNumberToUuid.ts @@ -0,0 +1,75 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250103192244_idNumberToUuid extends Migration { + + override async up(): Promise { + this.addSql(`alter table "exercise" drop constraint "exercise_exercise_type_id_foreign";`); + this.addSql(`alter table "exercise" drop constraint "exercise_video_id_foreign";`); + + this.addSql(`alter table "exercise_type" alter column "id" drop default;`); + this.addSql(`alter table "exercise_type" alter column "id" type uuid using ("id"::text::uuid);`); + this.addSql(`alter table "exercise_type" alter column "id" set default gen_random_uuid();`); + + this.addSql(`alter table "test" alter column "id" drop default;`); + this.addSql(`alter table "test" alter column "id" type uuid using ("id"::text::uuid);`); + this.addSql(`alter table "test" alter column "id" set default gen_random_uuid();`); + + this.addSql(`alter table "video" alter column "id" drop default;`); + this.addSql(`alter table "video" alter column "id" type uuid using ("id"::text::uuid);`); + this.addSql(`alter table "video" alter column "id" set default gen_random_uuid();`); + + this.addSql(`alter table "exercise" alter column "id" drop default;`); + this.addSql(`alter table "exercise" alter column "id" type uuid using ("id"::text::uuid);`); + this.addSql(`alter table "exercise" alter column "id" set default gen_random_uuid();`); + this.addSql(`alter table "exercise" alter column "exercise_type_id" drop default;`); + this.addSql(`alter table "exercise" alter column "exercise_type_id" type uuid using ("exercise_type_id"::text::uuid);`); + this.addSql(`alter table "exercise" alter column "video_id" drop default;`); + this.addSql(`alter table "exercise" alter column "video_id" type uuid using ("video_id"::text::uuid);`); + this.addSql(`alter table "exercise" add constraint "exercise_exercise_type_id_foreign" foreign key ("exercise_type_id") references "exercise_type" ("id") on update cascade;`); + this.addSql(`alter table "exercise" add constraint "exercise_video_id_foreign" foreign key ("video_id") references "video" ("id") on update cascade on delete set null;`); + } + + override async down(): Promise { + this.addSql(`alter table "exercise_type" alter column "id" type text using ("id"::text);`); + + this.addSql(`alter table "test" alter column "id" type text using ("id"::text);`); + + this.addSql(`alter table "video" alter column "id" type text using ("id"::text);`); + + this.addSql(`alter table "exercise" alter column "id" type text using ("id"::text);`); + this.addSql(`alter table "exercise" alter column "exercise_type_id" type text using ("exercise_type_id"::text);`); + this.addSql(`alter table "exercise" alter column "video_id" type text using ("video_id"::text);`); + + this.addSql(`alter table "exercise" drop constraint "exercise_exercise_type_id_foreign";`); + this.addSql(`alter table "exercise" drop constraint "exercise_video_id_foreign";`); + + this.addSql(`alter table "exercise_type" alter column "id" drop default;`); + this.addSql(`alter table "exercise_type" alter column "id" type int using ("id"::int);`); + this.addSql(`create sequence if not exists "exercise_type_id_seq";`); + this.addSql(`select setval('exercise_type_id_seq', (select max("id") from "exercise_type"));`); + this.addSql(`alter table "exercise_type" alter column "id" set default nextval('exercise_type_id_seq');`); + + this.addSql(`alter table "test" alter column "id" drop default;`); + this.addSql(`alter table "test" alter column "id" type int using ("id"::int);`); + this.addSql(`create sequence if not exists "test_id_seq";`); + this.addSql(`select setval('test_id_seq', (select max("id") from "test"));`); + this.addSql(`alter table "test" alter column "id" set default nextval('test_id_seq');`); + + this.addSql(`alter table "video" alter column "id" drop default;`); + this.addSql(`alter table "video" alter column "id" type int using ("id"::int);`); + this.addSql(`create sequence if not exists "video_id_seq";`); + this.addSql(`select setval('video_id_seq', (select max("id") from "video"));`); + this.addSql(`alter table "video" alter column "id" set default nextval('video_id_seq');`); + + this.addSql(`alter table "exercise" alter column "id" drop default;`); + this.addSql(`alter table "exercise" alter column "id" type int using ("id"::int);`); + this.addSql(`alter table "exercise" alter column "exercise_type_id" type int using ("exercise_type_id"::int);`); + this.addSql(`alter table "exercise" alter column "video_id" type int using ("video_id"::int);`); + this.addSql(`create sequence if not exists "exercise_id_seq";`); + this.addSql(`select setval('exercise_id_seq', (select max("id") from "exercise"));`); + this.addSql(`alter table "exercise" alter column "id" set default nextval('exercise_id_seq');`); + this.addSql(`alter table "exercise" add constraint "exercise_exercise_type_id_foreign" foreign key ("exercise_type_id") references "exercise_type" ("id") on update cascade;`); + this.addSql(`alter table "exercise" add constraint "exercise_video_id_foreign" foreign key ("video_id") references "video" ("id") on update cascade on delete set null;`); + } + +} diff --git a/apps/api/src/modules/exercise/exercise.controller.ts b/apps/api/src/modules/exercise/exercise.controller.ts index de7fd98..424598e 100644 --- a/apps/api/src/modules/exercise/exercise.controller.ts +++ b/apps/api/src/modules/exercise/exercise.controller.ts @@ -50,9 +50,7 @@ export class ExerciseController implements NestControllerInterface { // Dans le contrat, pathParams = { id: z.string() } // => on cast en number (ou on utilise z.coerce.number() dans le contrat) try { - const id = parseInt(params.id, 10); - - const exercise = await this.exerciseService.getExercise(id); + const exercise = await this.exerciseService.getExercise(params.id); return { status: 200 as const, @@ -102,12 +100,9 @@ export class ExerciseController implements NestControllerInterface { async updateExercise( @TsRestRequest() { params, body }: RequestShapes['updateExercise'] ) { - // Todo: revoir si je passe en number ou string dans le contrat try { - const id = parseInt(params.id, 10); - const updatedExercise = await this.exerciseService.updateExercise( - id, + params.id, body ); @@ -132,9 +127,7 @@ export class ExerciseController implements NestControllerInterface { @TsRestRequest() { params }: RequestShapes['deleteExercise'] ) { try { - const id = parseInt(params.id, 10); - - await this.exerciseService.deleteExercise(id); + await this.exerciseService.deleteExercise(params.id); return { status: 200 as const, diff --git a/apps/api/src/modules/exercise/exercise.service.ts b/apps/api/src/modules/exercise/exercise.service.ts index 7c808b8..64cee31 100644 --- a/apps/api/src/modules/exercise/exercise.service.ts +++ b/apps/api/src/modules/exercise/exercise.service.ts @@ -41,7 +41,7 @@ export class ExerciseService { }); } - async getExercise(id: number): Promise { + async getExercise(id: string): Promise { const exercise = await this.em.findOne( Exercise, { id }, @@ -121,7 +121,7 @@ export class ExerciseService { } async updateExercise( - id: number, + id: string, exercise: UpdateExercise ): Promise { const exerciseToUpdate = await this.em.findOne( @@ -168,7 +168,7 @@ export class ExerciseService { }; } - async deleteExercise(id: number): Promise<{ message: string }> { + async deleteExercise(id: string): Promise<{ message: string }> { const exerciseToDelete = await this.em.findOne(Exercise, { id }); if (!exerciseToDelete) { diff --git a/apps/api/src/modules/exerciseType/exerciseType.controller.ts b/apps/api/src/modules/exerciseType/exerciseType.controller.ts index a51bf5a..66849d0 100644 --- a/apps/api/src/modules/exerciseType/exerciseType.controller.ts +++ b/apps/api/src/modules/exerciseType/exerciseType.controller.ts @@ -20,7 +20,7 @@ export class ExerciseTypeController { } @Get(':id') - async getExerciseType(@Param('id') id: number) { + async getExerciseType(@Param('id') id: string) { return this.exerciseTypeService.getExerciseType(id); } @@ -31,14 +31,14 @@ export class ExerciseTypeController { @Put(':id') async updateExerciseType( - @Param('id') id: number, + @Param('id') id: string, @Body() exerciseType: ExerciseType ) { return this.exerciseTypeService.updateExerciseType(id, exerciseType); } @Delete(':id') - async deleteExerciseType(@Param('id') id: number) { + async deleteExerciseType(@Param('id') id: string) { return this.exerciseTypeService.deleteExerciseType(id); } } diff --git a/apps/api/src/modules/exerciseType/exerciseType.service.ts b/apps/api/src/modules/exerciseType/exerciseType.service.ts index 5eb37fa..b392b65 100644 --- a/apps/api/src/modules/exerciseType/exerciseType.service.ts +++ b/apps/api/src/modules/exerciseType/exerciseType.service.ts @@ -11,7 +11,7 @@ export class ExerciseTypeService { return this.em.find(ExerciseType, {}); } - async getExerciseType(id: number) { + async getExerciseType(id: string) { return this.em.findOne(ExerciseType, { id }); } @@ -22,7 +22,7 @@ export class ExerciseTypeService { return exerciseTypeToCreate; } - async updateExerciseType(id: number, exerciseType: ExerciseTypeDto) { + async updateExerciseType(id: string, exerciseType: ExerciseTypeDto) { const exerciseTypeToUpdate = await this.em.findOne(ExerciseType, { id }); if (!exerciseTypeToUpdate) { @@ -35,7 +35,7 @@ export class ExerciseTypeService { return exerciseTypeToUpdate; } - async deleteExerciseType(id: number) { + async deleteExerciseType(id: string) { const exerciseTypeToDelete = await this.em.findOne(ExerciseType, { id }); if (!exerciseTypeToDelete) { diff --git a/apps/api/src/modules/video/video.controller.ts b/apps/api/src/modules/video/video.controller.ts index dad15be..f7faecf 100644 --- a/apps/api/src/modules/video/video.controller.ts +++ b/apps/api/src/modules/video/video.controller.ts @@ -20,7 +20,7 @@ export class VideoController { } @Get(':id') - async getVideo(@Param('id') id: number) { + async getVideo(@Param('id') id: string) { return this.videoService.getVideo(id); } @@ -30,12 +30,12 @@ export class VideoController { } @Put(':id') - async updateVideo(@Param('id') id: number, @Body() video: Video) { + async updateVideo(@Param('id') id: string, @Body() video: Video) { return this.videoService.updateVideo(id, video); } @Delete(':id') - async deleteVideo(@Param('id') id: number) { + async deleteVideo(@Param('id') id: string) { return this.videoService.deleteVideo(id); } } diff --git a/apps/api/src/modules/video/video.service.ts b/apps/api/src/modules/video/video.service.ts index 364ef8d..a8d3e55 100644 --- a/apps/api/src/modules/video/video.service.ts +++ b/apps/api/src/modules/video/video.service.ts @@ -10,7 +10,7 @@ export class VideoService { return this.em.find(Video, {}); } - async getVideo(id: number) { + async getVideo(id: string) { return this.em.findOne(Video, { id }); } @@ -18,7 +18,7 @@ export class VideoService { return this.em.persistAndFlush(video); } - async updateVideo(id: number, video: Video) { + async updateVideo(id: string, video: Video) { const videoToUpdate = await this.em.findOne(Video, { id }); if (!videoToUpdate) { @@ -29,7 +29,7 @@ export class VideoService { await this.em.flush(); } - async deleteVideo(id: number) { + async deleteVideo(id: string) { const videoToDelete = await this.em.findOne(Video, { id }); if (!videoToDelete) { diff --git a/apps/api/test/exercise.e2e-spec.ts b/apps/api/test/exercise.e2e-spec.ts index 93c78ba..436e8de 100644 --- a/apps/api/test/exercise.e2e-spec.ts +++ b/apps/api/test/exercise.e2e-spec.ts @@ -169,7 +169,9 @@ describe('ExerciseController (e2e)', () => { describe('Error cases', () => { it('GET :id - should return 404 for non-existent exercise', async () => { - await request(app.getHttpServer()).get('/exercise/99999').expect(404); + await request(app.getHttpServer()) + .get('/exercise/123e4567-e89b-12d3-a456-426614174000') + .expect(404); }); it('PATCH :id - should return 404 for non-existent exercise', async () => { @@ -179,14 +181,14 @@ describe('ExerciseController (e2e)', () => { }; await request(app.getHttpServer()) - .patch('/exercise/99999') + .patch('/exercise/123e4567-e89b-12d3-a456-426614174000') .send(updateData) .expect(404); }); it('DELETE :id - should return 404 for non-existent exercise', async () => { await request(app.getHttpServer()) - .delete('/exercise/99999') + .delete('/exercise/123e4567-e89b-12d3-a456-426614174000') .expect(404); }); diff --git a/packages/schemas/src/exercice.schema.ts b/packages/schemas/src/exercice.schema.ts index 4bb2ea0..767f0ad 100644 --- a/packages/schemas/src/exercice.schema.ts +++ b/packages/schemas/src/exercice.schema.ts @@ -3,8 +3,8 @@ import { z } from 'zod'; export const createExerciseSchema = z.object({ name: z.string(), description: z.string().optional(), - exerciseType: z.number(), - video: z.number().optional(), + exerciseType: z.string(), + video: z.string().optional(), englishName: z.string().optional(), shortName: z.string().optional(), }); @@ -16,13 +16,13 @@ export const updateExerciseSchema = createExerciseSchema.partial(); export type UpdateExercise = z.infer; export const exerciseTypeSchema = z.object({ - id: z.number(), + id: z.string(), name: z.string(), exerciseType: z.object({ - id: z.number(), + id: z.string(), name: z.string(), }), - video: z.number().optional(), + video: z.string().optional(), description: z.string().optional(), englishName: z.string().optional(), shortName: z.string().optional(),