Add song & search history (#165)

This commit is contained in:
Zoe Roux
2023-03-01 13:29:33 +09:00
committed by GitHub
parent df85d0cfa7
commit f1a3f6e46a
30 changed files with 553 additions and 126 deletions

4
.envrc Normal file
View 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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -15,6 +15,16 @@ 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 {
@@ -29,6 +39,18 @@ model Song {
genreId Int?
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 {

View File

@@ -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],

View 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";
}

View 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>
}

View 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();
});
});

View 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);
}
}

View 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 {}

View 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();
});
});

View 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,
})
}
}

View File

@@ -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<Song | null> {
const ret = await this.searchService.songByTitle({ name });
@UseGuards(JwtAuthGuard)
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();
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<any[] | null> {
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();

View File

@@ -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],

View File

@@ -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<Song | null> {
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<Song[]> {
async guessSong(word: string, userID: number): Promise<Song[]> {
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<Artist[]> {
async guessArtist(word: string, userID: number): Promise<Artist[]> {
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<Album[]> {
async guessAlbum(word: string, userID: number): Promise<Album[]> {
await this.history.createSearchHistoryRecord({ query: word, type: "album", userID });
return this.prisma.album.findMany({
where: {
name: { contains: word },

View File

@@ -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';

View 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": ""}

View File

@@ -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 ***

View 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}

View File

@@ -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
@@ -43,3 +45,6 @@ services:
- "back"
env_file:
- .env
volumes:
db:

43
flake.lock generated Normal file
View 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
View 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
'';
};
});
}

View File

@@ -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:

View File

@@ -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 })