add smart random

This commit is contained in:
2023-07-14 14:54:47 +02:00
parent 68942e904f
commit b14fb39009
25 changed files with 509 additions and 104 deletions

View File

@@ -22,13 +22,15 @@ import {
GetAllDecksCommand,
GetDeckByIdCommand,
UpdateDeckCommand,
CreateCardCommand,
} from './use-cases'
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
import { GetAllDecksDto } from './dto/get-all-decks.dto'
import { GetAllCardsInDeckDto } from '../cards/dto/get-all-cards.dto'
import { CreateCardCommand } from './use-cases'
import { CreateCardDto } from '../cards/dto/create-card.dto'
import { Pagination } from '../../infrastructure/common/pagination/pagination.service'
import { GetRandomCardInDeckCommand } from './use-cases/get-random-card-in-deck-use-case'
import { SaveGradeCommand } from './use-cases/save-grade-use-case'
@Controller('decks')
export class DecksController {
@@ -57,9 +59,21 @@ export class DecksController {
@UseGuards(JwtAuthGuard)
@Get(':id/cards')
findCardsInDeck(@Param('id') id: string, @Req() req, @Query() query: GetAllCardsInDeckDto) {
return this.commandBus.execute(new GetAllCardsInDeckCommand(req.user.id, id, query))
const finalQuery = Pagination.getPaginationData(query)
return this.commandBus.execute(new GetAllCardsInDeckCommand(req.user.id, id, finalQuery))
}
@UseGuards(JwtAuthGuard)
@Get(':id/learn')
findRandomCardInDeck(@Param('id') id: string, @Req() req) {
return this.commandBus.execute(new GetRandomCardInDeckCommand(req.user.id, id))
}
@UseGuards(JwtAuthGuard)
@Post(':id/learn')
saveGrade(@Param('id') id: string, @Req() req, @Body() body: any) {
return this.commandBus.execute(
new SaveGradeCommand(req.user.id, { cardId: body.cardId, grade: body.grade })
)
}
@UseGuards(JwtAuthGuard)
@Post(':id/cards')
createCardInDeck(@Param('id') id: string, @Req() req, @Body() card: CreateCardDto) {

View File

@@ -13,21 +13,26 @@ import {
} from './use-cases'
import { DecksRepository } from './infrastructure/decks.repository'
import { CardsRepository } from '../cards/infrastructure/cards.repository'
import { GetRandomCardInDeckHandler } from './use-cases/get-random-card-in-deck-use-case'
import { GradesRepository } from './infrastructure/grades.repository'
import { SaveGradeHandler } from './use-cases/save-grade-use-case'
const commandHandlers = [
CreateDeckHandler,
GetAllDecksHandler,
GetDeckByIdHandler,
GetRandomCardInDeckHandler,
DeleteDeckByIdHandler,
UpdateDeckHandler,
GetAllCardsInDeckHandler,
CreateCardHandler,
SaveGradeHandler,
]
@Module({
imports: [CqrsModule],
controllers: [DecksController],
providers: [DecksService, DecksRepository, CardsRepository, ...commandHandlers],
providers: [DecksService, DecksRepository, CardsRepository, GradesRepository, ...commandHandlers],
exports: [CqrsModule],
})
export class DecksModule {}

View File

@@ -0,0 +1,10 @@
import { IsUUID, Max, Min } from 'class-validator'
export class CreateDeckDto {
@Min(1)
@Max(5)
grade: number
@IsUUID()
cardId: string
}

View File

@@ -1,6 +1,7 @@
import { IsUUID } from 'class-validator'
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators/is-optional-or-empty-string'
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
import { IsOrderBy } from '../../../infrastructure/decorators/is-order-by-constraint'
export class GetAllDecksDto extends PaginationDto {
@IsOptionalOrEmptyString()
@@ -17,4 +18,7 @@ export class GetAllDecksDto extends PaginationDto {
authorId?: string
userId: string
@IsOrderBy()
orderBy?: string | null
}

View File

@@ -2,6 +2,7 @@ import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common
import { PrismaService } from '../../../prisma.service'
import { GetAllDecksDto } from '../dto/get-all-decks.dto'
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
import { createPrismaOrderBy } from '../../../infrastructure/common/helpers/get-order-by-object'
@Injectable()
export class DecksRepository {
@@ -48,13 +49,15 @@ export class DecksRepository {
itemsPerPage,
minCardsCount,
maxCardsCount,
orderBy,
}: GetAllDecksDto) {
console.log({ name, authorId, userId, currentPage, itemsPerPage, minCardsCount, maxCardsCount })
console.log(minCardsCount)
console.log(Number(minCardsCount))
try {
const where = {
cardsCount: {
gte: Number(minCardsCount) ?? undefined,
lte: Number(maxCardsCount) ?? undefined,
gte: minCardsCount ? Number(minCardsCount) : undefined,
lte: maxCardsCount ? Number(maxCardsCount) : undefined,
},
name: {
contains: name,
@@ -85,9 +88,7 @@ export class DecksRepository {
}),
this.prisma.deck.findMany({
where,
orderBy: {
created: 'desc',
},
orderBy: createPrismaOrderBy(orderBy),
include: {
author: {
select: {
@@ -124,6 +125,24 @@ export class DecksRepository {
throw new InternalServerErrorException(e?.message)
}
}
public async findDeckByCardId(cardId: string) {
try {
const card = await this.prisma.card.findUnique({
where: {
id: cardId,
},
})
return await this.prisma.deck.findUnique({
where: {
id: card.deckId,
},
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
public async deleteDeckById(id: string) {
try {

View File

@@ -0,0 +1,59 @@
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
import { PrismaService } from '../../../prisma.service'
@Injectable()
export class GradesRepository {
constructor(private prisma: PrismaService) {}
private readonly logger = new Logger(GradesRepository.name)
async createGrade({
cardId,
userId,
deckId,
grade,
}: {
cardId: string
userId: string
deckId: string
grade: number
}) {
try {
return await this.prisma.grade.upsert({
where: {
userId,
cardId,
deckId,
},
update: {
grade,
shots: {
increment: 1,
},
},
create: {
grade,
shots: 1,
user: {
connect: {
id: userId,
},
},
card: {
connect: {
id: cardId,
},
},
deck: {
connect: {
id: deckId,
},
},
},
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
}

View File

@@ -2,6 +2,7 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
import { GetAllCardsInDeckDto } from '../../cards/dto/get-all-cards.dto'
import { ForbiddenException, NotFoundException } from '@nestjs/common'
import { DecksRepository } from '../infrastructure/decks.repository'
export class GetAllCardsInDeckCommand {
constructor(
@@ -13,10 +14,13 @@ export class GetAllCardsInDeckCommand {
@CommandHandler(GetAllCardsInDeckCommand)
export class GetAllCardsInDeckHandler implements ICommandHandler<GetAllCardsInDeckCommand> {
constructor(private readonly cardsRepository: CardsRepository) {}
constructor(
private readonly cardsRepository: CardsRepository,
private readonly decksRepository: DecksRepository
) {}
async execute(command: GetAllCardsInDeckCommand) {
const deck = await this.cardsRepository.findDeckById(command.deckId)
const deck = await this.decksRepository.findDeckById(command.deckId)
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
if (deck.userId !== command.userId && deck.isPrivate) {

View File

@@ -0,0 +1,53 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
import { ForbiddenException, NotFoundException } from '@nestjs/common'
import { DecksRepository } from '../infrastructure/decks.repository'
import { Prisma } from '@prisma/client'
import { pick } from 'remeda'
export class GetRandomCardInDeckCommand {
constructor(public readonly userId: string, public readonly deckId: string) {}
}
type CardWithGrade = Prisma.cardGetPayload<{ include: { grades: true } }>
@CommandHandler(GetRandomCardInDeckCommand)
export class GetRandomCardInDeckHandler implements ICommandHandler<GetRandomCardInDeckCommand> {
constructor(
private readonly cardsRepository: CardsRepository,
private readonly decksRepository: DecksRepository
) {}
private async getSmartRandomCard(cards: Array<CardWithGrade>) {
const selectionPool: Array<CardWithGrade> = []
cards.forEach(card => {
// Calculate the average grade for the card
const averageGrade =
card.grades.length === 0
? 0
: card.grades.reduce((acc, grade) => acc + grade.grade, 0) / card.grades.length
// Calculate weight for the card, higher weight for lower grade card
const weight = 6 - averageGrade
// Add the card to the selection pool `weight` times
for (let i = 0; i < weight; i++) {
selectionPool.push(card)
}
})
return selectionPool[Math.floor(Math.random() * selectionPool.length)]
}
async execute(command: GetRandomCardInDeckCommand) {
const deck = await this.decksRepository.findDeckById(command.deckId)
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
if (deck.userId !== command.userId && deck.isPrivate) {
throw new ForbiddenException(`You can't get a private deck that you don't own`)
}
const cards = await this.cardsRepository.findCardsByDeckIdWithGrade(
command.userId,
command.deckId
)
const smartRandomCard = await this.getSmartRandomCard(cards)
return pick(smartRandomCard, ['id', 'question', 'answer', 'deckId'])
}
}

View File

@@ -0,0 +1,41 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
import { ForbiddenException, NotFoundException } from '@nestjs/common'
import { DecksRepository } from '../infrastructure/decks.repository'
import { GradesRepository } from '../infrastructure/grades.repository'
export class SaveGradeCommand {
constructor(
public readonly userId: string,
public readonly args: {
cardId: string
grade: number
}
) {}
}
@CommandHandler(SaveGradeCommand)
export class SaveGradeHandler implements ICommandHandler<SaveGradeCommand> {
constructor(
private readonly cardsRepository: CardsRepository,
private readonly decksRepository: DecksRepository,
private readonly gradesRepository: GradesRepository
) {}
async execute(command: SaveGradeCommand) {
const deck = await this.decksRepository.findDeckByCardId(command.args.cardId)
if (!deck)
throw new NotFoundException(`Deck containing card with id ${command.args.cardId} not found`)
if (deck.userId !== command.userId && deck.isPrivate) {
throw new ForbiddenException(`You can't save cards to a private deck that you don't own`)
}
return await this.gradesRepository.createGrade({
userId: command.userId,
grade: command.args.grade,
cardId: command.args.cardId,
deckId: deck.id,
})
}
}