Add song & search history (#165)
This commit is contained in:
4
.envrc
Normal file
4
.envrc
Normal file
@@ -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
|
||||||
78
back/package-lock.json
generated
78
back/package-lock.json
generated
@@ -17,7 +17,7 @@
|
|||||||
"@nestjs/passport": "^8.2.2",
|
"@nestjs/passport": "^8.2.2",
|
||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
"@nestjs/swagger": "^5.2.1",
|
"@nestjs/swagger": "^5.2.1",
|
||||||
"@prisma/client": "^3.14.0",
|
"@prisma/client": "^4.4.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/passport": "^1.0.9",
|
"@types/passport": "^1.0.9",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"jest": "^27.2.5",
|
"jest": "^27.2.5",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"prisma": "^3.13.0",
|
"prisma": "^4.4.0",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
@@ -1683,15 +1683,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "3.14.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.4.0.tgz",
|
||||||
"integrity": "sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA==",
|
"integrity": "sha512-ciKOP246x1xwr04G9ajHlJ4pkmtu9Q6esVyqVBO0QJihaKQIUvbPjClp17IsRJyxqNpFm4ScbOc/s9DUzKHINQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines-version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a"
|
"@prisma/engines-version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.6"
|
"node": ">=14.17"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"prisma": "*"
|
"prisma": "*"
|
||||||
@@ -1703,16 +1703,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.4.0.tgz",
|
||||||
"integrity": "sha512-Ip9CcCeUocH61eXu4BUGpvl5KleQyhcUVLpWCv+0ZmDv44bFaDpREqjGHHdRupvPN/ugB6gTlD9b9ewdj02yVA==",
|
"integrity": "sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true
|
"hasInstallScript": true
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a",
|
"version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz",
|
||||||
"integrity": "sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ=="
|
"integrity": "sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug=="
|
||||||
},
|
},
|
||||||
"node_modules/@sinonjs/commons": {
|
"node_modules/@sinonjs/commons": {
|
||||||
"version": "1.8.3",
|
"version": "1.8.3",
|
||||||
@@ -7304,21 +7304,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "3.13.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-3.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.4.0.tgz",
|
||||||
"integrity": "sha512-oO1auBnBtieGdiN+57IgsA9Vr7Sy4HkILi1KSaUG4mpKfEbnkTGnLOxAqjLed+K2nsG/GtE1tJBtB7JxN1a78Q==",
|
"integrity": "sha512-l/QKLmLcKJQFuc+X02LyICo0NWTUVaNNZ00jKJBqwDyhwMAhboD1FWwYV50rkH4Wls0RviAJSFzkC2ZrfawpfA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines": "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b",
|
"@prisma/engines": "4.4.0"
|
||||||
"ts-pattern": "^4.0.1"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js",
|
"prisma": "build/index.js",
|
||||||
"prisma2": "build/index.js"
|
"prisma2": "build/index.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.6"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
@@ -8618,12 +8617,6 @@
|
|||||||
"node": ">=0.4.0"
|
"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": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
||||||
@@ -10435,23 +10428,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@prisma/client": {
|
"@prisma/client": {
|
||||||
"version": "3.14.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.4.0.tgz",
|
||||||
"integrity": "sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA==",
|
"integrity": "sha512-ciKOP246x1xwr04G9ajHlJ4pkmtu9Q6esVyqVBO0QJihaKQIUvbPjClp17IsRJyxqNpFm4ScbOc/s9DUzKHINQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@prisma/engines-version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a"
|
"@prisma/engines-version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@prisma/engines": {
|
"@prisma/engines": {
|
||||||
"version": "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.4.0.tgz",
|
||||||
"integrity": "sha512-Ip9CcCeUocH61eXu4BUGpvl5KleQyhcUVLpWCv+0ZmDv44bFaDpREqjGHHdRupvPN/ugB6gTlD9b9ewdj02yVA==",
|
"integrity": "sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"@prisma/engines-version": {
|
"@prisma/engines-version": {
|
||||||
"version": "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a",
|
"version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz",
|
||||||
"integrity": "sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ=="
|
"integrity": "sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug=="
|
||||||
},
|
},
|
||||||
"@sinonjs/commons": {
|
"@sinonjs/commons": {
|
||||||
"version": "1.8.3",
|
"version": "1.8.3",
|
||||||
@@ -14767,13 +14760,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"version": "3.13.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-3.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.4.0.tgz",
|
||||||
"integrity": "sha512-oO1auBnBtieGdiN+57IgsA9Vr7Sy4HkILi1KSaUG4mpKfEbnkTGnLOxAqjLed+K2nsG/GtE1tJBtB7JxN1a78Q==",
|
"integrity": "sha512-l/QKLmLcKJQFuc+X02LyICo0NWTUVaNNZ00jKJBqwDyhwMAhboD1FWwYV50rkH4Wls0RviAJSFzkC2ZrfawpfA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@prisma/engines": "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b",
|
"@prisma/engines": "4.4.0"
|
||||||
"ts-pattern": "^4.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"process-nextick-args": {
|
"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": {
|
"tsconfig-paths": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@nestjs/passport": "^8.2.2",
|
"@nestjs/passport": "^8.2.2",
|
||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
"@nestjs/swagger": "^5.2.1",
|
"@nestjs/swagger": "^5.2.1",
|
||||||
"@prisma/client": "^3.14.0",
|
"@prisma/client": "^4.4.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/passport": "^1.0.9",
|
"@types/passport": "^1.0.9",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"jest": "^27.2.5",
|
"jest": "^27.2.5",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"prisma": "^3.13.0",
|
"prisma": "^4.4.0",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
14
back/prisma/migrations/20230227022300_/migration.sql
Normal file
14
back/prisma/migrations/20230227022300_/migration.sql
Normal file
@@ -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;
|
||||||
11
back/prisma/migrations/20230228020324_/migration.sql
Normal file
11
back/prisma/migrations/20230228020324_/migration.sql
Normal file
@@ -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;
|
||||||
8
back/prisma/migrations/20230228022322_/migration.sql
Normal file
8
back/prisma/migrations/20230228022322_/migration.sql
Normal file
@@ -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;
|
||||||
5
back/prisma/migrations/20230301021027_/migration.sql
Normal file
5
back/prisma/migrations/20230301021027_/migration.sql
Normal file
@@ -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;
|
||||||
@@ -15,20 +15,42 @@ model User {
|
|||||||
password String
|
password String
|
||||||
email String
|
email String
|
||||||
LessonHistory LessonHistory[]
|
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 {
|
model Song {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
midiPath String
|
midiPath String
|
||||||
musicXmlPath String
|
musicXmlPath String
|
||||||
artistId Int?
|
artistId Int?
|
||||||
artist Artist? @relation(fields: [artistId], references: [id])
|
artist Artist? @relation(fields: [artistId], references: [id])
|
||||||
albumId Int?
|
albumId Int?
|
||||||
album Album? @relation(fields: [albumId], references: [id])
|
album Album? @relation(fields: [albumId], references: [id])
|
||||||
genreId Int?
|
genreId Int?
|
||||||
genre Genre? @relation(fields: [genreId], references: [id])
|
genre Genre? @relation(fields: [genreId], references: [id])
|
||||||
difficulties Json
|
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 {
|
model Genre {
|
||||||
@@ -42,16 +64,16 @@ model Artist {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
|
|
||||||
Song Song[]
|
Song Song[]
|
||||||
Album Album[]
|
Album Album[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Album {
|
model Album {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
artistId Int?
|
artistId Int?
|
||||||
artist Artist? @relation(fields: [artistId], references: [id])
|
artist Artist? @relation(fields: [artistId], references: [id])
|
||||||
Song Song[]
|
Song Song[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Lesson {
|
model Lesson {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { AlbumModule } from './album/album.module';
|
|||||||
import { SearchController } from './search/search.controller';
|
import { SearchController } from './search/search.controller';
|
||||||
import { SearchService } from './search/search.service';
|
import { SearchService } from './search/search.service';
|
||||||
import { SearchModule } from './search/search.module';
|
import { SearchModule } from './search/search.module';
|
||||||
|
import { HistoryModule } from './history/history.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -27,6 +28,7 @@ import { SearchModule } from './search/search.module';
|
|||||||
ArtistModule,
|
ArtistModule,
|
||||||
AlbumModule,
|
AlbumModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
|
HistoryModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, PrismaService, ArtistService],
|
providers: [AppService, PrismaService, ArtistService],
|
||||||
|
|||||||
14
back/src/history/dto/SearchHistoryDto.ts
Normal file
14
back/src/history/dto/SearchHistoryDto.ts
Normal file
@@ -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";
|
||||||
|
}
|
||||||
19
back/src/history/dto/SongHistoryDto.ts
Normal file
19
back/src/history/dto/SongHistoryDto.ts
Normal file
@@ -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<string, number>
|
||||||
|
}
|
||||||
18
back/src/history/history.controller.spec.ts
Normal file
18
back/src/history/history.controller.spec.ts
Normal file
@@ -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>(HistoryController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
42
back/src/history/history.controller.ts
Normal file
42
back/src/history/history.controller.ts
Normal file
@@ -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<SongHistory[]> {
|
||||||
|
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<SearchHistory[]> {
|
||||||
|
return this.historyService.getSearchHistory(req.user.id, { skip, take });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HttpCode(201)
|
||||||
|
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
|
||||||
|
return this.historyService.createSongHistoryRecord(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
back/src/history/history.module.ts
Normal file
12
back/src/history/history.module.ts
Normal file
@@ -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 {}
|
||||||
18
back/src/history/history.service.spec.ts
Normal file
18
back/src/history/history.service.spec.ts
Normal file
@@ -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>(HistoryService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
59
back/src/history/history.service.ts
Normal file
59
back/src/history/history.service.ts
Normal file
@@ -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<SongHistory> {
|
||||||
|
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<SongHistory[]> {
|
||||||
|
return this.prisma.songHistory.findMany({
|
||||||
|
where: { user: { id: playerId } },
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSearchHistoryRecord({ userID, query, type }: SearchHistoryDto): Promise<SearchHistory> {
|
||||||
|
return this.prisma.searchHistory.create({
|
||||||
|
data: {
|
||||||
|
query,
|
||||||
|
type,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: userID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSearchHistory(playerId: number, { skip, take }: { skip?: number, take?: number }): Promise<SearchHistory[]> {
|
||||||
|
return this.prisma.searchHistory.findMany({
|
||||||
|
where: { user: { id: playerId } },
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,39 +2,35 @@ import {
|
|||||||
BadRequestException,
|
BadRequestException,
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
DefaultValuePipe,
|
|
||||||
Get,
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Param,
|
Param,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Request,
|
||||||
Req,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
||||||
import { Song } from '@prisma/client';
|
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 { SearchSongDto } from './dto/search-song.dto';
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
|
|
||||||
@ApiTags('search')
|
@ApiTags('search')
|
||||||
@Controller('search')
|
@Controller('search')
|
||||||
export class SearchController {
|
export class SearchController {
|
||||||
constructor(
|
constructor(private readonly searchService: SearchService) { }
|
||||||
private readonly searchService: SearchService,
|
|
||||||
private readonly songService: SongService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get a song details by song name',
|
summary: 'Get a song details by song name',
|
||||||
description: 'Get a song details by song name',
|
description: 'Get a song details by song name',
|
||||||
})
|
})
|
||||||
@Get('song/:name')
|
@Get('song/:name')
|
||||||
async findByName(@Param('name') name: string): Promise<Song | null> {
|
@UseGuards(JwtAuthGuard)
|
||||||
const ret = await this.searchService.songByTitle({ name });
|
async findByName(@Request() req: any, @Param('name') name: string): Promise<Song | null> {
|
||||||
|
const ret = await this.searchService.songByTitle({ name }, req.user?.id);
|
||||||
if (!ret) throw new NotFoundException();
|
if (!ret) throw new NotFoundException();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -113,20 +109,22 @@ export class SearchController {
|
|||||||
example: 'Yoko Shimomura',
|
example: 'Yoko Shimomura',
|
||||||
})
|
})
|
||||||
@ApiParam({ name: 'type', type: 'string', required: true, example: 'artist' })
|
@ApiParam({ name: 'type', type: 'string', required: true, example: 'artist' })
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
async guess(
|
async guess(
|
||||||
|
@Request() req: any,
|
||||||
@Param() params: { type: string; word: string },
|
@Param() params: { type: string; word: string },
|
||||||
): Promise<any[] | null> {
|
): Promise<any[] | null> {
|
||||||
try {
|
try {
|
||||||
let ret: any[];
|
let ret: any[];
|
||||||
switch (params.type) {
|
switch (params.type) {
|
||||||
case 'artist':
|
case 'artist':
|
||||||
ret = await this.searchService.guessArtist(params.word);
|
ret = await this.searchService.guessArtist(params.word, req.user?.id);
|
||||||
break;
|
break;
|
||||||
case 'album':
|
case 'album':
|
||||||
ret = await this.searchService.guessAlbum(params.word);
|
ret = await this.searchService.guessAlbum(params.word, req.user?.id);
|
||||||
break;
|
break;
|
||||||
case 'song':
|
case 'song':
|
||||||
ret = await this.searchService.guessSong(params.word);
|
ret = await this.searchService.guessSong(params.word, req.user?.id);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
import { SearchController } from './search.controller';
|
import { SearchController } from './search.controller';
|
||||||
|
import { HistoryModule } from 'src/history/history.module';
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
import { SongService } from 'src/song/song.service';
|
import { SongService } from 'src/song/song.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, HistoryModule],
|
||||||
controllers: [SearchController],
|
controllers: [SearchController],
|
||||||
providers: [SearchService, SongService],
|
providers: [SearchService, SongService],
|
||||||
exports: [SearchService],
|
exports: [SearchService],
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
DefaultValuePipe,
|
|
||||||
Injectable,
|
|
||||||
ParseIntPipe,
|
|
||||||
Query,
|
|
||||||
Req,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Album, Artist, Prisma, Song } from '@prisma/client';
|
import { Album, Artist, Prisma, Song } from '@prisma/client';
|
||||||
|
import { HistoryService } from 'src/history/history.service';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService, private history: HistoryService) { }
|
||||||
|
|
||||||
async songByTitle(
|
async songByTitle(
|
||||||
songWhereUniqueInput: Prisma.SongWhereUniqueInput,
|
songWhereUniqueInput: Prisma.SongWhereUniqueInput,
|
||||||
|
userID: number
|
||||||
): Promise<Song | null> {
|
): Promise<Song | null> {
|
||||||
|
if (songWhereUniqueInput.name)
|
||||||
|
await this.history.createSearchHistoryRecord({ query: songWhereUniqueInput.name, userID, type: "song" });
|
||||||
return this.prisma.song.findUnique({
|
return this.prisma.song.findUnique({
|
||||||
where: songWhereUniqueInput,
|
where: songWhereUniqueInput,
|
||||||
});
|
});
|
||||||
@@ -53,7 +51,8 @@ export class SearchService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async guessSong(word: string): Promise<Song[]> {
|
async guessSong(word: string, userID: number): Promise<Song[]> {
|
||||||
|
await this.history.createSearchHistoryRecord({ query: word, type: "song", userID });
|
||||||
return this.prisma.song.findMany({
|
return this.prisma.song.findMany({
|
||||||
where: {
|
where: {
|
||||||
name: { contains: word },
|
name: { contains: word },
|
||||||
@@ -61,7 +60,8 @@ export class SearchService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async guessArtist(word: string): Promise<Artist[]> {
|
async guessArtist(word: string, userID: number): Promise<Artist[]> {
|
||||||
|
await this.history.createSearchHistoryRecord({ query: word, type: "artist", userID });
|
||||||
return this.prisma.artist.findMany({
|
return this.prisma.artist.findMany({
|
||||||
where: {
|
where: {
|
||||||
name: { contains: word },
|
name: { contains: word },
|
||||||
@@ -69,7 +69,8 @@ export class SearchService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async guessAlbum(word: string): Promise<Album[]> {
|
async guessAlbum(word: string, userID: number): Promise<Album[]> {
|
||||||
|
await this.history.createSearchHistoryRecord({ query: word, type: "album", userID });
|
||||||
return this.prisma.album.findMany({
|
return this.prisma.album.findMany({
|
||||||
where: {
|
where: {
|
||||||
name: { contains: word },
|
name: { contains: word },
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
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 { User, Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import * as bcrypt from 'bcryptjs';
|
import * as bcrypt from 'bcryptjs';
|
||||||
|
|||||||
47
back/test/robot/auth/auth.resource
Normal file
47
back/test/robot/auth/auth.resource
Normal file
@@ -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": ""}
|
||||||
@@ -1,31 +1,9 @@
|
|||||||
*** Settings ***
|
*** Settings ***
|
||||||
Documentation Tests of the /auth route.
|
Documentation Tests of the /auth route.
|
||||||
... Ensures that the user can authenticate on kyoo.
|
... Ensures that the user can authenticate on kyoo.
|
||||||
|
|
||||||
Resource ../rest.resource
|
Resource ../rest.resource
|
||||||
|
Resource ./auth.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": ""}
|
|
||||||
|
|
||||||
|
|
||||||
*** Test Cases ***
|
*** Test Cases ***
|
||||||
@@ -43,7 +21,7 @@ Bad Account
|
|||||||
RegisterAndLogin
|
RegisterAndLogin
|
||||||
[Documentation] Create a new user and login in it
|
[Documentation] Create a new user and login in it
|
||||||
Register user-1
|
Register user-1
|
||||||
Login user-1
|
Login user-1
|
||||||
[Teardown] DELETE /auth/me
|
[Teardown] DELETE /auth/me
|
||||||
|
|
||||||
Register Duplicates
|
Register Duplicates
|
||||||
@@ -53,13 +31,13 @@ Register Duplicates
|
|||||||
POST /auth/register {"username": "user-duplicate", "password": "pass", "email": "mail@kyoo.moe"}
|
POST /auth/register {"username": "user-duplicate", "password": "pass", "email": "mail@kyoo.moe"}
|
||||||
Output
|
Output
|
||||||
Integer response status 400
|
Integer response status 400
|
||||||
Login user-duplicate
|
Login user-duplicate
|
||||||
[Teardown] DELETE /auth/me
|
[Teardown] DELETE /auth/me
|
||||||
|
|
||||||
Delete Account
|
Delete Account
|
||||||
[Documentation] Check if a user can delete it's account
|
[Documentation] Check if a user can delete it's account
|
||||||
Register I-should-be-deleted
|
Register I-should-be-deleted
|
||||||
Login I-should-be-deleted
|
Login I-should-be-deleted
|
||||||
DELETE /auth/me
|
DELETE /auth/me
|
||||||
Output
|
Output
|
||||||
Integer response status 200
|
Integer response status 200
|
||||||
@@ -67,7 +45,7 @@ Delete Account
|
|||||||
Login
|
Login
|
||||||
[Documentation] Create a new user and login in it
|
[Documentation] Create a new user and login in it
|
||||||
Register login-user
|
Register login-user
|
||||||
Login login-user
|
Login login-user
|
||||||
${res}= GET /auth/me
|
${res}= GET /auth/me
|
||||||
Output
|
Output
|
||||||
Integer response status 200
|
Integer response status 200
|
||||||
|
|||||||
55
back/test/robot/history/history.robot
Normal file
55
back/test/robot/history/history.robot
Normal file
@@ -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}
|
||||||
@@ -25,6 +25,8 @@ services:
|
|||||||
- POSTGRES_DB=${POSTGRES_DB}
|
- POSTGRES_DB=${POSTGRES_DB}
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- db:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@@ -32,7 +34,7 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
front:
|
front:
|
||||||
build:
|
build:
|
||||||
context: ./front
|
context: ./front
|
||||||
args:
|
args:
|
||||||
- API_URL=${API_URL}
|
- API_URL=${API_URL}
|
||||||
@@ -43,3 +45,6 @@ services:
|
|||||||
- "back"
|
- "back"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db:
|
||||||
|
|||||||
43
flake.lock
generated
Normal file
43
flake.lock
generated
Normal file
@@ -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
|
||||||
|
}
|
||||||
28
flake.nix
Normal file
28
flake.nix
Normal file
@@ -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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -17,9 +17,15 @@ channels:
|
|||||||
type:
|
type:
|
||||||
type: "string"
|
type: "string"
|
||||||
enum: ["start"]
|
enum: ["start"]
|
||||||
name:
|
id:
|
||||||
|
type: "number"
|
||||||
|
description: "The id of the song"
|
||||||
|
mode:
|
||||||
type: "string"
|
type: "string"
|
||||||
description: "The name of the song"
|
enum: ["practice", "normal"]
|
||||||
|
user_id:
|
||||||
|
type: "number"
|
||||||
|
description: "The ID of the user playing"
|
||||||
operationId: "startSong"
|
operationId: "startSong"
|
||||||
/midi:
|
/midi:
|
||||||
publish:
|
publish:
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class Scorometer():
|
|||||||
|
|
||||||
def getTimingScore(self, key: Key, to_play: Key):
|
def getTimingScore(self, key: Key, to_play: Key):
|
||||||
tempo_percent = abs((key.duration / to_play.duration) - 1)
|
tempo_percent = abs((key.duration / to_play.duration) - 1)
|
||||||
if tempo_percent < .3 :
|
if tempo_percent < .3:
|
||||||
timingScore = "perfect"
|
timingScore = "perfect"
|
||||||
elif tempo_percent < .5:
|
elif tempo_percent < .5:
|
||||||
timingScore = f"great"
|
timingScore = f"great"
|
||||||
@@ -161,9 +161,6 @@ class Scorometer():
|
|||||||
if obj["type"] == "pause":
|
if obj["type"] == "pause":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def sendEnd(self, overall, difficulties):
|
|
||||||
send({"overallScore": overall, "score": difficulties})
|
|
||||||
|
|
||||||
def sendScore(self, id, timingScore, timingInformation):
|
def sendScore(self, id, timingScore, timingInformation):
|
||||||
send({"id": id, "timingScore": timingScore, "timingInformation": timingInformation})
|
send({"id": id, "timingScore": timingScore, "timingInformation": timingInformation})
|
||||||
|
|
||||||
@@ -177,7 +174,7 @@ class Scorometer():
|
|||||||
self.handleMessage(line.rstrip())
|
self.handleMessage(line.rstrip())
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
self.sendEnd(self.score, {})
|
return self.score, {}
|
||||||
|
|
||||||
def handleStartMessage(start_message):
|
def handleStartMessage(start_message):
|
||||||
if "type" not in start_message.keys():
|
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")
|
raise Exception("id of song not specified in start message")
|
||||||
if "mode" not in start_message.keys():
|
if "mode" not in start_message.keys():
|
||||||
raise Exception("mode of song not specified in start message")
|
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
|
mode = PRACTICE if start_message["mode"] == "practice" else NORMAL
|
||||||
# TODO get song path from the API
|
# TODO get song path from the API
|
||||||
song_id = start_message["id"]
|
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"]
|
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():
|
def main():
|
||||||
try:
|
try:
|
||||||
start_message = json.loads(input())
|
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 = Scorometer(mode, song_path)
|
||||||
sc.gameLoop()
|
score, difficulties = sc.gameLoop()
|
||||||
|
sendScore(score, difficulties, song_id, user_id)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
send({ "error": error })
|
send({ "error": error })
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user