From f1a3f6e46aef196097333bef6b2fd94266e111f2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 1 Mar 2023 13:29:33 +0900 Subject: [PATCH] Add song & search history (#165) --- .envrc | 4 + back/package-lock.json | 78 ++++++++----------- back/package.json | 4 +- .../20230225102626_song_history/migration.sql | 13 ++++ .../migration.sql | 8 ++ .../migrations/20230227022300_/migration.sql | 14 ++++ .../migrations/20230228020324_/migration.sql | 11 +++ .../migrations/20230228022322_/migration.sql | 8 ++ .../migrations/20230301021027_/migration.sql | 5 ++ back/prisma/schema.prisma | 44 ++++++++--- back/src/app.module.ts | 2 + back/src/history/dto/SearchHistoryDto.ts | 14 ++++ back/src/history/dto/SongHistoryDto.ts | 19 +++++ back/src/history/history.controller.spec.ts | 18 +++++ back/src/history/history.controller.ts | 42 ++++++++++ back/src/history/history.module.ts | 12 +++ back/src/history/history.service.spec.ts | 18 +++++ back/src/history/history.service.ts | 59 ++++++++++++++ back/src/search/search.controller.ts | 26 +++---- back/src/search/search.module.ts | 3 +- back/src/search/search.service.ts | 23 +++--- back/src/users/users.service.ts | 2 - back/test/robot/auth/auth.resource | 47 +++++++++++ back/test/robot/auth/auth.robot | 34 ++------ back/test/robot/history/history.robot | 55 +++++++++++++ docker-compose.yml | 7 +- flake.lock | 43 ++++++++++ flake.nix | 28 +++++++ scorometer/asyncapi.spec.yml | 10 ++- scorometer/main.py | 28 +++++-- 30 files changed, 553 insertions(+), 126 deletions(-) create mode 100644 .envrc create mode 100644 back/prisma/migrations/20230225102626_song_history/migration.sql create mode 100644 back/prisma/migrations/20230226012434_add_history_s_score/migration.sql create mode 100644 back/prisma/migrations/20230227022300_/migration.sql create mode 100644 back/prisma/migrations/20230228020324_/migration.sql create mode 100644 back/prisma/migrations/20230228022322_/migration.sql create mode 100644 back/prisma/migrations/20230301021027_/migration.sql create mode 100644 back/src/history/dto/SearchHistoryDto.ts create mode 100644 back/src/history/dto/SongHistoryDto.ts create mode 100644 back/src/history/history.controller.spec.ts create mode 100644 back/src/history/history.controller.ts create mode 100644 back/src/history/history.module.ts create mode 100644 back/src/history/history.service.spec.ts create mode 100644 back/src/history/history.service.ts create mode 100644 back/test/robot/auth/auth.resource create mode 100644 back/test/robot/history/history.robot create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..e31c306 --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" +fi +use flake diff --git a/back/package-lock.json b/back/package-lock.json index a2b1dee..a41cbe8 100644 --- a/back/package-lock.json +++ b/back/package-lock.json @@ -17,7 +17,7 @@ "@nestjs/passport": "^8.2.2", "@nestjs/platform-express": "^8.0.0", "@nestjs/swagger": "^5.2.1", - "@prisma/client": "^3.14.0", + "@prisma/client": "^4.4.0", "@types/bcrypt": "^5.0.0", "@types/bcryptjs": "^2.4.2", "@types/passport": "^1.0.9", @@ -46,7 +46,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "^27.2.5", "prettier": "^2.3.2", - "prisma": "^3.13.0", + "prisma": "^4.4.0", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "^27.0.3", @@ -1683,15 +1683,15 @@ } }, "node_modules/@prisma/client": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.14.0.tgz", - "integrity": "sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.4.0.tgz", + "integrity": "sha512-ciKOP246x1xwr04G9ajHlJ4pkmtu9Q6esVyqVBO0QJihaKQIUvbPjClp17IsRJyxqNpFm4ScbOc/s9DUzKHINQ==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + "@prisma/engines-version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6" }, "engines": { - "node": ">=12.6" + "node": ">=14.17" }, "peerDependencies": { "prisma": "*" @@ -1703,16 +1703,16 @@ } }, "node_modules/@prisma/engines": { - "version": "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b.tgz", - "integrity": "sha512-Ip9CcCeUocH61eXu4BUGpvl5KleQyhcUVLpWCv+0ZmDv44bFaDpREqjGHHdRupvPN/ugB6gTlD9b9ewdj02yVA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.4.0.tgz", + "integrity": "sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==", "devOptional": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz", - "integrity": "sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ==" + "version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz", + "integrity": "sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug==" }, "node_modules/@sinonjs/commons": { "version": "1.8.3", @@ -7304,21 +7304,20 @@ } }, "node_modules/prisma": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.13.0.tgz", - "integrity": "sha512-oO1auBnBtieGdiN+57IgsA9Vr7Sy4HkILi1KSaUG4mpKfEbnkTGnLOxAqjLed+K2nsG/GtE1tJBtB7JxN1a78Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.4.0.tgz", + "integrity": "sha512-l/QKLmLcKJQFuc+X02LyICo0NWTUVaNNZ00jKJBqwDyhwMAhboD1FWwYV50rkH4Wls0RviAJSFzkC2ZrfawpfA==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b", - "ts-pattern": "^4.0.1" + "@prisma/engines": "4.4.0" }, "bin": { "prisma": "build/index.js", "prisma2": "build/index.js" }, "engines": { - "node": ">=12.6" + "node": ">=14.17" } }, "node_modules/process-nextick-args": { @@ -8618,12 +8617,6 @@ "node": ">=0.4.0" } }, - "node_modules/ts-pattern": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.0.2.tgz", - "integrity": "sha512-eHqR/7A6fcw05vCOfnL6RwgGJbVi9G/YHTdYdjYmElhDdJ1SMn7pWs+6+YuxygaFwQS/g+cIDlu+UD8IVpur1A==", - "devOptional": true - }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -10435,23 +10428,23 @@ } }, "@prisma/client": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.14.0.tgz", - "integrity": "sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.4.0.tgz", + "integrity": "sha512-ciKOP246x1xwr04G9ajHlJ4pkmtu9Q6esVyqVBO0QJihaKQIUvbPjClp17IsRJyxqNpFm4ScbOc/s9DUzKHINQ==", "requires": { - "@prisma/engines-version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" + "@prisma/engines-version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6" } }, "@prisma/engines": { - "version": "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b.tgz", - "integrity": "sha512-Ip9CcCeUocH61eXu4BUGpvl5KleQyhcUVLpWCv+0ZmDv44bFaDpREqjGHHdRupvPN/ugB6gTlD9b9ewdj02yVA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.4.0.tgz", + "integrity": "sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==", "devOptional": true }, "@prisma/engines-version": { - "version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz", - "integrity": "sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ==" + "version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz", + "integrity": "sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug==" }, "@sinonjs/commons": { "version": "1.8.3", @@ -14767,13 +14760,12 @@ } }, "prisma": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.13.0.tgz", - "integrity": "sha512-oO1auBnBtieGdiN+57IgsA9Vr7Sy4HkILi1KSaUG4mpKfEbnkTGnLOxAqjLed+K2nsG/GtE1tJBtB7JxN1a78Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.4.0.tgz", + "integrity": "sha512-l/QKLmLcKJQFuc+X02LyICo0NWTUVaNNZ00jKJBqwDyhwMAhboD1FWwYV50rkH4Wls0RviAJSFzkC2ZrfawpfA==", "devOptional": true, "requires": { - "@prisma/engines": "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b", - "ts-pattern": "^4.0.1" + "@prisma/engines": "4.4.0" } }, "process-nextick-args": { @@ -15715,12 +15707,6 @@ } } }, - "ts-pattern": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.0.2.tgz", - "integrity": "sha512-eHqR/7A6fcw05vCOfnL6RwgGJbVi9G/YHTdYdjYmElhDdJ1SMn7pWs+6+YuxygaFwQS/g+cIDlu+UD8IVpur1A==", - "devOptional": true - }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", diff --git a/back/package.json b/back/package.json index 3670688..2be88c8 100644 --- a/back/package.json +++ b/back/package.json @@ -29,7 +29,7 @@ "@nestjs/passport": "^8.2.2", "@nestjs/platform-express": "^8.0.0", "@nestjs/swagger": "^5.2.1", - "@prisma/client": "^3.14.0", + "@prisma/client": "^4.4.0", "@types/bcrypt": "^5.0.0", "@types/bcryptjs": "^2.4.2", "@types/passport": "^1.0.9", @@ -58,7 +58,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "^27.2.5", "prettier": "^2.3.2", - "prisma": "^3.13.0", + "prisma": "^4.4.0", "source-map-support": "^0.5.20", "supertest": "^6.1.3", "ts-jest": "^27.0.3", diff --git a/back/prisma/migrations/20230225102626_song_history/migration.sql b/back/prisma/migrations/20230225102626_song_history/migration.sql new file mode 100644 index 0000000..6b5c3e8 --- /dev/null +++ b/back/prisma/migrations/20230225102626_song_history/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "SongHistory" ( + "songID" INTEGER NOT NULL, + "userID" INTEGER NOT NULL, + + CONSTRAINT "SongHistory_pkey" PRIMARY KEY ("songID","userID") +); + +-- AddForeignKey +ALTER TABLE "SongHistory" ADD CONSTRAINT "SongHistory_songID_fkey" FOREIGN KEY ("songID") REFERENCES "Song"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SongHistory" ADD CONSTRAINT "SongHistory_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/back/prisma/migrations/20230226012434_add_history_s_score/migration.sql b/back/prisma/migrations/20230226012434_add_history_s_score/migration.sql new file mode 100644 index 0000000..78dbd7d --- /dev/null +++ b/back/prisma/migrations/20230226012434_add_history_s_score/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `score` to the `SongHistory` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "SongHistory" ADD COLUMN "score" INTEGER NOT NULL; diff --git a/back/prisma/migrations/20230227022300_/migration.sql b/back/prisma/migrations/20230227022300_/migration.sql new file mode 100644 index 0000000..4d52e4f --- /dev/null +++ b/back/prisma/migrations/20230227022300_/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - Added the required column `difficulties` to the `SongHistory` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "SongHistory" DROP CONSTRAINT "SongHistory_userID_fkey"; + +-- AlterTable +ALTER TABLE "SongHistory" ADD COLUMN "difficulties" JSONB NOT NULL; + +-- AddForeignKey +ALTER TABLE "SongHistory" ADD CONSTRAINT "SongHistory_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/back/prisma/migrations/20230228020324_/migration.sql b/back/prisma/migrations/20230228020324_/migration.sql new file mode 100644 index 0000000..05291e6 --- /dev/null +++ b/back/prisma/migrations/20230228020324_/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "SearchHistory" ( + "id" SERIAL NOT NULL, + "query" TEXT NOT NULL, + "userId" INTEGER, + + CONSTRAINT "SearchHistory_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "SearchHistory" ADD CONSTRAINT "SearchHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/back/prisma/migrations/20230228022322_/migration.sql b/back/prisma/migrations/20230228022322_/migration.sql new file mode 100644 index 0000000..9678b19 --- /dev/null +++ b/back/prisma/migrations/20230228022322_/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `type` to the `SearchHistory` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "SearchHistory" ADD COLUMN "type" TEXT NOT NULL; diff --git a/back/prisma/migrations/20230301021027_/migration.sql b/back/prisma/migrations/20230301021027_/migration.sql new file mode 100644 index 0000000..9e880c8 --- /dev/null +++ b/back/prisma/migrations/20230301021027_/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "SongHistory" DROP CONSTRAINT "SongHistory_songID_fkey"; + +-- AddForeignKey +ALTER TABLE "SongHistory" ADD CONSTRAINT "SongHistory_songID_fkey" FOREIGN KEY ("songID") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/back/prisma/schema.prisma b/back/prisma/schema.prisma index 5b6d0b2..037006c 100644 --- a/back/prisma/schema.prisma +++ b/back/prisma/schema.prisma @@ -15,20 +15,42 @@ model User { password String email String LessonHistory LessonHistory[] + SongHistory SongHistory[] + searchHistory SearchHistory[] +} + +model SearchHistory { + id Int @id @default(autoincrement()) + query String + type String + userId Int? + user User? @relation(fields: [userId], references: [id]) } model Song { - id Int @id @default(autoincrement()) - name String @unique + id Int @id @default(autoincrement()) + name String @unique midiPath String musicXmlPath String artistId Int? - artist Artist? @relation(fields: [artistId], references: [id]) + artist Artist? @relation(fields: [artistId], references: [id]) albumId Int? - album Album? @relation(fields: [albumId], references: [id]) + album Album? @relation(fields: [albumId], references: [id]) genreId Int? - genre Genre? @relation(fields: [genreId], references: [id]) + genre Genre? @relation(fields: [genreId], references: [id]) difficulties Json + SongHistory SongHistory[] +} + +model SongHistory { + song Song @relation(fields: [songID], references: [id], onDelete: Cascade, onUpdate: Cascade) + songID Int + user User @relation(fields: [userID], references: [id], onDelete: Cascade, onUpdate: Cascade) + userID Int + score Int + difficulties Json + + @@id([songID, userID]) } model Genre { @@ -42,16 +64,16 @@ model Artist { id Int @id @default(autoincrement()) name String @unique - Song Song[] + Song Song[] Album Album[] } model Album { - id Int @id @default(autoincrement()) - name String @unique - artistId Int? - artist Artist? @relation(fields: [artistId], references: [id]) - Song Song[] + id Int @id @default(autoincrement()) + name String @unique + artistId Int? + artist Artist? @relation(fields: [artistId], references: [id]) + Song Song[] } model Lesson { diff --git a/back/src/app.module.ts b/back/src/app.module.ts index a5e526e..5f98408 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -15,6 +15,7 @@ import { AlbumModule } from './album/album.module'; import { SearchController } from './search/search.controller'; import { SearchService } from './search/search.service'; import { SearchModule } from './search/search.module'; +import { HistoryModule } from './history/history.module'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { SearchModule } from './search/search.module'; ArtistModule, AlbumModule, SearchModule, + HistoryModule, ], controllers: [AppController], providers: [AppService, PrismaService, ArtistService], diff --git a/back/src/history/dto/SearchHistoryDto.ts b/back/src/history/dto/SearchHistoryDto.ts new file mode 100644 index 0000000..efd6f82 --- /dev/null +++ b/back/src/history/dto/SearchHistoryDto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber } from "class-validator"; + +export class SearchHistoryDto { + @ApiProperty() + @IsNumber() + userID: number; + + @ApiProperty() + query: string; + + @ApiProperty() + type: "song" | "artist" | "album"; +} diff --git a/back/src/history/dto/SongHistoryDto.ts b/back/src/history/dto/SongHistoryDto.ts new file mode 100644 index 0000000..31eff6c --- /dev/null +++ b/back/src/history/dto/SongHistoryDto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber } from "class-validator"; + +export class SongHistoryDto { + @ApiProperty() + @IsNumber() + songID: number; + + @ApiProperty() + @IsNumber() + userID: number; + + @ApiProperty() + @IsNumber() + score: number; + + @ApiProperty() + difficulties: Record +} diff --git a/back/src/history/history.controller.spec.ts b/back/src/history/history.controller.spec.ts new file mode 100644 index 0000000..71ab2f4 --- /dev/null +++ b/back/src/history/history.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HistoryController } from './history.controller'; + +describe('HistoryController', () => { + let controller: HistoryController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [HistoryController], + }).compile(); + + controller = module.get(HistoryController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/back/src/history/history.controller.ts b/back/src/history/history.controller.ts new file mode 100644 index 0000000..750911d --- /dev/null +++ b/back/src/history/history.controller.ts @@ -0,0 +1,42 @@ +import { Body, Controller, DefaultValuePipe, Get, HttpCode, ParseIntPipe, Post, Query, Request, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { SearchHistory, SongHistory } from '@prisma/client'; +import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; +import { SongHistoryDto } from './dto/SongHistoryDto'; +import { HistoryService } from './history.service'; + +@Controller('history') +@ApiTags("history") +export class HistoryController { + constructor(private readonly historyService: HistoryService) {} + + @Get() + @HttpCode(200) + @UseGuards(JwtAuthGuard) + @ApiUnauthorizedResponse({ description: 'Invalid token' }) + async getHistory( + @Request() req: any, + @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number, + @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number, + ): Promise { + return this.historyService.getHistory(req.user.id, { skip, take }); + } + + @Get("search") + @HttpCode(200) + @UseGuards(JwtAuthGuard) + @ApiUnauthorizedResponse({ description: 'Invalid token' }) + async getSearchHistory( + @Request() req: any, + @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number, + @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number, + ): Promise { + return this.historyService.getSearchHistory(req.user.id, { skip, take }); + } + + @Post() + @HttpCode(201) + async create(@Body() record: SongHistoryDto): Promise { + return this.historyService.createSongHistoryRecord(record); + } +} diff --git a/back/src/history/history.module.ts b/back/src/history/history.module.ts new file mode 100644 index 0000000..fc1bee3 --- /dev/null +++ b/back/src/history/history.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from 'src/prisma/prisma.module'; +import { HistoryService } from './history.service'; +import { HistoryController } from './history.controller'; + +@Module({ + imports: [PrismaModule], + providers: [HistoryService], + controllers: [HistoryController], + exports: [HistoryService], +}) +export class HistoryModule {} diff --git a/back/src/history/history.service.spec.ts b/back/src/history/history.service.spec.ts new file mode 100644 index 0000000..b79a1c6 --- /dev/null +++ b/back/src/history/history.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HistoryService } from './history.service'; + +describe('HistoryService', () => { + let service: HistoryService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [HistoryService], + }).compile(); + + service = module.get(HistoryService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/back/src/history/history.service.ts b/back/src/history/history.service.ts new file mode 100644 index 0000000..4cb4872 --- /dev/null +++ b/back/src/history/history.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { SearchHistory, SongHistory } from '@prisma/client'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { SearchHistoryDto } from './dto/SearchHistoryDto'; +import { SongHistoryDto } from './dto/SongHistoryDto'; + +@Injectable() +export class HistoryService { + constructor(private prisma: PrismaService) { } + + async createSongHistoryRecord({ songID, userID, score, difficulties }: SongHistoryDto): Promise { + return this.prisma.songHistory.create({ + data: { + score, + difficulties, + song: { + connect: { + id: songID, + }, + }, + user: { + connect: { + id: userID, + }, + }, + } + }); + } + + async getHistory(playerId: number, { skip, take }: { skip?: number, take?: number }): Promise { + return this.prisma.songHistory.findMany({ + where: { user: { id: playerId } }, + skip, + take, + }) + } + + async createSearchHistoryRecord({ userID, query, type }: SearchHistoryDto): Promise { + return this.prisma.searchHistory.create({ + data: { + query, + type, + user: { + connect: { + id: userID, + }, + }, + } + }); + } + + async getSearchHistory(playerId: number, { skip, take }: { skip?: number, take?: number }): Promise { + return this.prisma.searchHistory.findMany({ + where: { user: { id: playerId } }, + skip, + take, + }) + } +} diff --git a/back/src/search/search.controller.ts b/back/src/search/search.controller.ts index d719916..cc7e405 100644 --- a/back/src/search/search.controller.ts +++ b/back/src/search/search.controller.ts @@ -2,39 +2,35 @@ import { BadRequestException, Body, Controller, - DefaultValuePipe, Get, HttpCode, - HttpStatus, InternalServerErrorException, NotFoundException, Param, ParseIntPipe, Post, - Query, - Req, + Request, + UseGuards, } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { Song } from '@prisma/client'; -import { SongService } from 'src/song/song.service'; +import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; import { SearchSongDto } from './dto/search-song.dto'; import { SearchService } from './search.service'; @ApiTags('search') @Controller('search') export class SearchController { - constructor( - private readonly searchService: SearchService, - private readonly songService: SongService, - ) {} + constructor(private readonly searchService: SearchService) { } @ApiOperation({ summary: 'Get a song details by song name', description: 'Get a song details by song name', }) @Get('song/:name') - async findByName(@Param('name') name: string): Promise { - const ret = await this.searchService.songByTitle({ name }); + @UseGuards(JwtAuthGuard) + async findByName(@Request() req: any, @Param('name') name: string): Promise { + const ret = await this.searchService.songByTitle({ name }, req.user?.id); if (!ret) throw new NotFoundException(); return ret; } @@ -113,20 +109,22 @@ export class SearchController { example: 'Yoko Shimomura', }) @ApiParam({ name: 'type', type: 'string', required: true, example: 'artist' }) + @UseGuards(JwtAuthGuard) async guess( + @Request() req: any, @Param() params: { type: string; word: string }, ): Promise { try { let ret: any[]; switch (params.type) { case 'artist': - ret = await this.searchService.guessArtist(params.word); + ret = await this.searchService.guessArtist(params.word, req.user?.id); break; case 'album': - ret = await this.searchService.guessAlbum(params.word); + ret = await this.searchService.guessAlbum(params.word, req.user?.id); break; case 'song': - ret = await this.searchService.guessSong(params.word); + ret = await this.searchService.guessSong(params.word, req.user?.id); break; default: throw new BadRequestException(); diff --git a/back/src/search/search.module.ts b/back/src/search/search.module.ts index cd3e557..983fbb1 100644 --- a/back/src/search/search.module.ts +++ b/back/src/search/search.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { SearchService } from './search.service'; import { SearchController } from './search.controller'; +import { HistoryModule } from 'src/history/history.module'; import { PrismaModule } from 'src/prisma/prisma.module'; import { SongService } from 'src/song/song.service'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, HistoryModule], controllers: [SearchController], providers: [SearchService, SongService], exports: [SearchService], diff --git a/back/src/search/search.service.ts b/back/src/search/search.service.ts index 8c84c7a..fb7c08c 100644 --- a/back/src/search/search.service.ts +++ b/back/src/search/search.service.ts @@ -1,20 +1,18 @@ -import { - DefaultValuePipe, - Injectable, - ParseIntPipe, - Query, - Req, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Album, Artist, Prisma, Song } from '@prisma/client'; +import { HistoryService } from 'src/history/history.service'; import { PrismaService } from 'src/prisma/prisma.service'; @Injectable() export class SearchService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService, private history: HistoryService) { } async songByTitle( songWhereUniqueInput: Prisma.SongWhereUniqueInput, + userID: number ): Promise { + if (songWhereUniqueInput.name) + await this.history.createSearchHistoryRecord({ query: songWhereUniqueInput.name, userID, type: "song" }); return this.prisma.song.findUnique({ where: songWhereUniqueInput, }); @@ -53,7 +51,8 @@ export class SearchService { }); } - async guessSong(word: string): Promise { + async guessSong(word: string, userID: number): Promise { + await this.history.createSearchHistoryRecord({ query: word, type: "song", userID }); return this.prisma.song.findMany({ where: { name: { contains: word }, @@ -61,7 +60,8 @@ export class SearchService { }); } - async guessArtist(word: string): Promise { + async guessArtist(word: string, userID: number): Promise { + await this.history.createSearchHistoryRecord({ query: word, type: "artist", userID }); return this.prisma.artist.findMany({ where: { name: { contains: word }, @@ -69,7 +69,8 @@ export class SearchService { }); } - async guessAlbum(word: string): Promise { + async guessAlbum(word: string, userID: number): Promise { + await this.history.createSearchHistoryRecord({ query: word, type: "album", userID }); return this.prisma.album.findMany({ where: { name: { contains: word }, diff --git a/back/src/users/users.service.ts b/back/src/users/users.service.ts index df3a75d..403c927 100644 --- a/back/src/users/users.service.ts +++ b/back/src/users/users.service.ts @@ -1,6 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { CreateUserDto } from './dto/create-user.dto'; -import { UpdateUserDto } from './dto/update-user.dto'; import { User, Prisma } from '@prisma/client'; import { PrismaService } from 'src/prisma/prisma.service'; import * as bcrypt from 'bcryptjs'; diff --git a/back/test/robot/auth/auth.resource b/back/test/robot/auth/auth.resource new file mode 100644 index 0000000..6f6064c --- /dev/null +++ b/back/test/robot/auth/auth.resource @@ -0,0 +1,47 @@ +*** Settings *** +Documentation Methods to login/register. + +Resource ../rest.resource + + +*** Keywords *** +Login + [Documentation] Shortcut to login with the given username for future requests + [Arguments] ${username} + &{res}= POST /auth/login {"username": "${username}", "password": "password-${username}"} + Output + Integer response status 200 + String response body access_token + Set Headers {"Authorization": "Bearer ${res.body.access_token}"} + +Register + [Documentation] Shortcut to register with the given username for future requests + [Arguments] ${username} + &{res}= POST + ... /auth/register + ... {"username": "${username}", "password": "password-${username}", "email": "${username}@chromacase.moe"} + Output + Integer response status 201 + +RegisterLogin + [Documentation] Shortcut to register with the given username for future requests + [Arguments] ${username} + POST + ... /auth/register + ... {"username": "${username}", "password": "password-${username}", "email": "${username}@chromacase.moe"} + Output + Integer response status 201 + &{res}= POST /auth/login {"username": "${username}", "password": "password-${username}"} + Output + Integer response status 200 + String response body access_token + Set Headers {"Authorization": "Bearer ${res.body.access_token}"} + + &{me}= GET /auth/me + Output + Integer response status 200 + RETURN ${me.body.id} + +Logout + [Documentation] Logout the current user, only the local client is affected. + Set Headers {"Authorization": ""} diff --git a/back/test/robot/auth/auth.robot b/back/test/robot/auth/auth.robot index 15d8176..08a7273 100644 --- a/back/test/robot/auth/auth.robot +++ b/back/test/robot/auth/auth.robot @@ -1,31 +1,9 @@ *** Settings *** Documentation Tests of the /auth route. ... Ensures that the user can authenticate on kyoo. + Resource ../rest.resource - - -*** Keywords *** -Login - [Documentation] Shortcut to login with the given username for future requests - [Arguments] ${username} - &{res}= POST /auth/login {"username": "${username}", "password": "password-${username}"} - Output - Integer response status 200 - String response body access_token - Set Headers {"Authorization": "Bearer ${res.body.access_token}"} - -Register - [Documentation] Shortcut to register with the given username for future requests - [Arguments] ${username} - &{res}= POST - ... /auth/register - ... {"username": "${username}", "password": "password-${username}", "email": "${username}@chromacase.moe"} - Output - Integer response status 201 - -Logout - [Documentation] Logout the current user, only the local client is affected. - Set Headers {"Authorization": ""} +Resource ./auth.resource *** Test Cases *** @@ -43,7 +21,7 @@ Bad Account RegisterAndLogin [Documentation] Create a new user and login in it Register user-1 - Login user-1 + Login user-1 [Teardown] DELETE /auth/me Register Duplicates @@ -53,13 +31,13 @@ Register Duplicates POST /auth/register {"username": "user-duplicate", "password": "pass", "email": "mail@kyoo.moe"} Output Integer response status 400 - Login user-duplicate + Login user-duplicate [Teardown] DELETE /auth/me Delete Account [Documentation] Check if a user can delete it's account Register I-should-be-deleted - Login I-should-be-deleted + Login I-should-be-deleted DELETE /auth/me Output Integer response status 200 @@ -67,7 +45,7 @@ Delete Account Login [Documentation] Create a new user and login in it Register login-user - Login login-user + Login login-user ${res}= GET /auth/me Output Integer response status 200 diff --git a/back/test/robot/history/history.robot b/back/test/robot/history/history.robot new file mode 100644 index 0000000..4306d06 --- /dev/null +++ b/back/test/robot/history/history.robot @@ -0,0 +1,55 @@ +*** Settings *** +Documentation Tests of the /history route. +... Ensures that the history CRUD works corectly. + +Resource ../rest.resource +Resource ../auth/auth.resource + + +*** Test Cases *** +Get history without behing connected + &{history}= GET /history + Output + Integer response status 401 + +Create and get an history record + [Documentation] Create an history item + &{song}= POST + ... /song + ... {"name": "Mama mia", "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"} + Output + ${userID}= RegisterLogin wowuser + + &{history}= POST + ... /history + ... { "userID": ${userID}, "songID": ${song.body.id}, "score": 55, "difficulties": {} } + Output + Integer response status 201 + + &{res}= GET /history + Output + Integer response status 200 + Array response body + Integer $[0].userID ${userID} + Integer $[0].songID ${song.body.id} + Integer $[0].score 55 + + [Teardown] Run Keywords DELETE /users/${userID} + ... AND DELETE /song/${song.body.id} + +Create and get a search history record + [Documentation] Create a search history item + ${userID}= RegisterLogin historyqueryuser + + GET /search/song/toto + Output + Integer response status 404 + + &{res}= GET /history/search + Output + Integer response status 200 + Array response body + String $[0].type "song" + String $[0].query "toto" + + [Teardown] DELETE /users/${userID} diff --git a/docker-compose.yml b/docker-compose.yml index b5d2737..9d1b361 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,8 @@ services: - POSTGRES_DB=${POSTGRES_DB} ports: - "5432:5432" + volumes: + - db:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s @@ -32,7 +34,7 @@ services: retries: 5 front: - build: + build: context: ./front args: - API_URL=${API_URL} @@ -43,3 +45,6 @@ services: - "back" env_file: - .env + +volumes: + db: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ea5eb0f --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1665573177, + "narHash": "sha256-Arkrf3zmi3lXYpbSe9H+HQxswQ6jxsAmeQVq5Sr/OZc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d2afb051ffd904af5a825f58abee3c63b148c5f2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "master", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..6b6a0fd --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + description = "A prisma test project"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/master"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in { + devShell = pkgs.mkShell { + nativeBuildInputs = [ pkgs.bashInteractive ]; + buildInputs = with pkgs; [ + nodePackages.prisma + nodePackages."@nestjs/cli" + nodePackages.npm + nodejs-slim + ]; + shellHook = with pkgs; '' + export PRISMA_MIGRATION_ENGINE_BINARY="${prisma-engines}/bin/migration-engine" + export PRISMA_QUERY_ENGINE_BINARY="${prisma-engines}/bin/query-engine" + export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node" + export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine" + export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt" + export DATABASE_URL=postgresql://user:eip@localhost:5432/chromacase + ''; + }; + }); +} diff --git a/scorometer/asyncapi.spec.yml b/scorometer/asyncapi.spec.yml index 1f688ea..79855ea 100644 --- a/scorometer/asyncapi.spec.yml +++ b/scorometer/asyncapi.spec.yml @@ -17,9 +17,15 @@ channels: type: type: "string" enum: ["start"] - name: + id: + type: "number" + description: "The id of the song" + mode: type: "string" - description: "The name of the song" + enum: ["practice", "normal"] + user_id: + type: "number" + description: "The ID of the user playing" operationId: "startSong" /midi: publish: diff --git a/scorometer/main.py b/scorometer/main.py index 24a9426..9db4cb0 100755 --- a/scorometer/main.py +++ b/scorometer/main.py @@ -133,7 +133,7 @@ class Scorometer(): def getTimingScore(self, key: Key, to_play: Key): tempo_percent = abs((key.duration / to_play.duration) - 1) - if tempo_percent < .3 : + if tempo_percent < .3: timingScore = "perfect" elif tempo_percent < .5: timingScore = f"great" @@ -161,9 +161,6 @@ class Scorometer(): if obj["type"] == "pause": pass - def sendEnd(self, overall, difficulties): - send({"overallScore": overall, "score": difficulties}) - def sendScore(self, id, timingScore, timingInformation): send({"id": id, "timingScore": timingScore, "timingInformation": timingInformation}) @@ -177,7 +174,7 @@ class Scorometer(): self.handleMessage(line.rstrip()) else: pass - self.sendEnd(self.score, {}) + return self.score, {} def handleStartMessage(start_message): if "type" not in start_message.keys(): @@ -188,19 +185,34 @@ def handleStartMessage(start_message): raise Exception("id of song not specified in start message") if "mode" not in start_message.keys(): raise Exception("mode of song not specified in start message") + if "user_id" not in start_message.keys(): + raise Exception("user_id not specified in start message") mode = PRACTICE if start_message["mode"] == "practice" else NORMAL # TODO get song path from the API song_id = start_message["id"] + # TODO: use something secure here but I don't find sending a jwt something elegant. + user_id = start_message["user_id"] song_path = requests.get(f"http://back:3000/song/{song_id}").json()["midiPath"] - return mode, song_path + return mode, song_path, song_id, user_id + + +def sendScore(score, difficulties, song_id, user_id): + send({"overallScore": score, "score": difficulties}) + requests.post(f"http://back:3000/history", json={ + "songID": song_id, + "userID": user_id, + "score": score, + "difficulties": difficulties, + }) def main(): try: start_message = json.loads(input()) - mode, song_path = handleStartMessage(start_message) + mode, song_path, song_id, user_id = handleStartMessage(start_message) sc = Scorometer(mode, song_path) - sc.gameLoop() + score, difficulties = sc.gameLoop() + sendScore(score, difficulties, song_id, user_id) except Exception as error: send({ "error": error })