add crete/get cards

This commit is contained in:
2023-06-18 12:16:03 +02:00
parent 0794238f0d
commit ca976cfe5d
19 changed files with 358 additions and 7 deletions

View File

@@ -82,23 +82,23 @@ model Card {
id String @id @default(cuid()) id String @id @default(cuid())
deckId String deckId String
userId String userId String
question String question String @db.Text
answer String answer String @db.Text
grade Int grade Int @default(0)
shots Int shots Int @default(0)
questionImg String? questionImg String?
answerImg String? answerImg String?
answerVideo String? answerVideo String?
questionVideo String? questionVideo String?
comments String? comments String?
type String? type String?
rating Int rating Int @default(0)
moreId String? moreId String?
created DateTime @default(now()) created DateTime @default(now())
updated DateTime @updatedAt updated DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
decks Deck @relation(fields: [deckId], references: [id], onDelete: Cascade) decks Deck @relation(fields: [deckId], references: [id], onDelete: Cascade)
grades Grade[] // One-to-many relation grades Grade[]
@@index([userId]) @@index([userId])
@@index([deckId]) @@index([deckId])

View File

@@ -9,6 +9,7 @@ import * as process from 'process'
import { JwtRefreshStrategy } from './modules/auth/strategies/jwt-refresh.strategy' import { JwtRefreshStrategy } from './modules/auth/strategies/jwt-refresh.strategy'
import { CqrsModule } from '@nestjs/cqrs' import { CqrsModule } from '@nestjs/cqrs'
import { DecksModule } from './modules/decks/decks.module' import { DecksModule } from './modules/decks/decks.module'
import { CardsModule } from './modules/cards/cards.module'
@Module({ @Module({
imports: [ imports: [
@@ -17,6 +18,7 @@ import { DecksModule } from './modules/decks/decks.module'
UsersModule, UsersModule,
AuthModule, AuthModule,
DecksModule, DecksModule,
CardsModule,
PrismaModule, PrismaModule,
MailerModule.forRoot({ MailerModule.forRoot({

View File

@@ -0,0 +1,29 @@
import { Body, Controller, Delete, Get, Param, Patch, Req, UseGuards } from '@nestjs/common'
import { CardsService } from './cards.service'
import { UpdateCardDto } from './dto/update-card.dto'
import { CommandBus } from '@nestjs/cqrs'
import { DeleteDeckByIdCommand, GetDeckByIdCommand, UpdateDeckCommand } from './use-cases'
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
@Controller('cards')
export class CardsController {
constructor(private readonly decksService: CardsService, private commandBus: CommandBus) {}
@UseGuards(JwtAuthGuard)
@Get(':id')
findOne(@Param('id') id: string) {
return this.commandBus.execute(new GetDeckByIdCommand(id))
}
@UseGuards(JwtAuthGuard)
@Patch(':id')
update(@Param('id') id: string, @Body() updateDeckDto: UpdateCardDto, @Req() req) {
return this.commandBus.execute(new UpdateDeckCommand(id, updateDeckDto, req.user.id))
}
@UseGuards(JwtAuthGuard)
@Delete(':id')
remove(@Param('id') id: string, @Req() req) {
return this.commandBus.execute(new DeleteDeckByIdCommand(id, req.user.id))
}
}

View File

@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common'
import { CardsService } from './cards.service'
import { CardsController } from './cards.controller'
import { CqrsModule } from '@nestjs/cqrs'
import {
CreateCardHandler,
DeleteDeckByIdHandler,
GetDeckByIdHandler,
GetAllCardsInDeckHandler,
UpdateDeckHandler,
} from './use-cases'
import { CardsRepository } from './infrastructure/cards.repository'
const commandHandlers = [
CreateCardHandler,
GetAllCardsInDeckHandler,
GetDeckByIdHandler,
DeleteDeckByIdHandler,
UpdateDeckHandler,
]
@Module({
imports: [CqrsModule],
controllers: [CardsController],
providers: [CardsService, CardsRepository, ...commandHandlers],
exports: [CqrsModule],
})
export class CardsModule {}

View File

@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common'
@Injectable()
export class CardsService {}

View File

@@ -0,0 +1,9 @@
import { Length } from 'class-validator'
export class CreateCardDto {
@Length(3, 500)
question: string
@Length(3, 500)
answer: string
}

View File

@@ -0,0 +1,13 @@
import { Length } from 'class-validator'
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators/is-optional-or-empty-string'
export class GetAllCardsInDeckDto extends PaginationDto {
@IsOptionalOrEmptyString()
@Length(1, 30)
question?: string
@IsOptionalOrEmptyString()
@Length(1, 30)
answer?: string
}

View File

@@ -0,0 +1,16 @@
import { PartialType } from '@nestjs/mapped-types'
import { CreateCardDto } from './create-card.dto'
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators/is-optional-or-empty-string'
import { IsBoolean } from 'class-validator'
export class UpdateCardDto extends PartialType(CreateCardDto) {
@IsOptionalOrEmptyString()
name: string
@IsOptionalOrEmptyString()
@IsBoolean()
isPrivate: boolean
@IsOptionalOrEmptyString()
cover: string
}

View File

@@ -0,0 +1 @@
export class Card {}

View File

@@ -0,0 +1,109 @@
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
import { PrismaService } from '../../../prisma.service'
import { GetAllCardsInDeckDto } from '../dto/get-all-cards.dto'
import {
DEFAULT_PAGE_NUMBER,
DEFAULT_PAGE_SIZE,
} from '../../../infrastructure/common/pagination/pagination.constants'
import { CreateCardDto } from '../dto/create-card.dto'
@Injectable()
export class CardsRepository {
constructor(private prisma: PrismaService) {}
private readonly logger = new Logger(CardsRepository.name)
async createCard(deckId: string, userId: string, card: CreateCardDto) {
try {
return await this.prisma.card.create({
data: {
user: {
connect: {
id: userId,
},
},
decks: {
connect: {
id: deckId,
},
},
...card,
},
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
async findCardsByDeckId(
deckId: string,
{
answer = undefined,
question = undefined,
currentPage = DEFAULT_PAGE_NUMBER,
pageSize = DEFAULT_PAGE_SIZE,
}: GetAllCardsInDeckDto
) {
try {
return await this.prisma.card.findMany({
where: {
decks: {
id: deckId,
},
question: {
contains: question || undefined,
},
answer: {
contains: answer || undefined,
},
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
public async findDeckById(id: string) {
try {
return await this.prisma.deck.findUnique({
where: {
id,
},
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
public async deleteDeckById(id: string) {
try {
return await this.prisma.deck.delete({
where: {
id,
},
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
public async updateDeckById(
id: string,
data: { name?: string; cover?: string; isPrivate?: boolean }
) {
try {
return await this.prisma.deck.update({
where: {
id,
},
data,
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
}

View File

@@ -0,0 +1,21 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { CardsRepository } from '../infrastructure/cards.repository'
import { BadRequestException, NotFoundException } from '@nestjs/common'
export class DeleteDeckByIdCommand {
constructor(public readonly id: string, public readonly userId: string) {}
}
@CommandHandler(DeleteDeckByIdCommand)
export class DeleteDeckByIdHandler implements ICommandHandler<DeleteDeckByIdCommand> {
constructor(private readonly deckRepository: CardsRepository) {}
async execute(command: DeleteDeckByIdCommand) {
const deck = await this.deckRepository.findDeckById(command.id)
if (!deck) throw new NotFoundException(`Deck with id ${command.id} not found`)
if (deck.userId !== command.userId) {
throw new BadRequestException(`You can't delete a deck that you don't own`)
}
return await this.deckRepository.deleteDeckById(command.id)
}
}

View File

@@ -0,0 +1,15 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { CardsRepository } from '../infrastructure/cards.repository'
export class GetDeckByIdCommand {
constructor(public readonly id: string) {}
}
@CommandHandler(GetDeckByIdCommand)
export class GetDeckByIdHandler implements ICommandHandler<GetDeckByIdCommand> {
constructor(private readonly deckRepository: CardsRepository) {}
async execute(command: GetDeckByIdCommand) {
return await this.deckRepository.findDeckById(command.id)
}
}

View File

@@ -0,0 +1,5 @@
export * from '../../decks/use-cases/create-card-use-case'
export * from '../../decks/use-cases/get-all-cards-in-deck-use-case'
export * from './get-deck-by-id-use-case'
export * from './delete-deck-by-id-use-case'
export * from './update-deck-use-case'

View File

@@ -0,0 +1,29 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { CardsRepository } from '../infrastructure/cards.repository'
import { UpdateCardDto } from '../dto/update-card.dto'
import { BadRequestException, NotFoundException } from '@nestjs/common'
export class UpdateDeckCommand {
constructor(
public readonly deckId: string,
public readonly deck: UpdateCardDto,
public readonly userId: string
) {}
}
@CommandHandler(UpdateDeckCommand)
export class UpdateDeckHandler implements ICommandHandler<UpdateDeckCommand> {
constructor(private readonly deckRepository: CardsRepository) {}
async execute(command: UpdateDeckCommand) {
const deck = await this.deckRepository.findDeckById(command.deckId)
if (!deck) throw new NotFoundException(`Deck with id ${command.deckId} not found`)
if (deck.userId !== command.userId) {
throw new BadRequestException(`You can't change a deck that you don't own`)
}
return await this.deckRepository.updateDeckById(command.deckId, command.deck)
}
}

View File

@@ -18,12 +18,16 @@ import { CommandBus } from '@nestjs/cqrs'
import { import {
CreateDeckCommand, CreateDeckCommand,
DeleteDeckByIdCommand, DeleteDeckByIdCommand,
GetAllCardsInDeckCommand,
GetAllDecksCommand, GetAllDecksCommand,
GetDeckByIdCommand, GetDeckByIdCommand,
UpdateDeckCommand, UpdateDeckCommand,
} from './use-cases' } from './use-cases'
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard' import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
import { GetAllDecksDto } from './dto/get-all-decks.dto' import { GetAllDecksDto } from './dto/get-all-decks.dto'
import { GetAllCardsInDeckDto } from '../cards/dto/get-all-cards.dto'
import { CreateCardCommand } from './use-cases/create-card-use-case'
import { CreateCardDto } from '../cards/dto/create-card.dto'
@Controller('decks') @Controller('decks')
export class DecksController { export class DecksController {
@@ -48,6 +52,18 @@ export class DecksController {
return this.commandBus.execute(new GetDeckByIdCommand(id)) return this.commandBus.execute(new GetDeckByIdCommand(id))
} }
@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))
}
@UseGuards(JwtAuthGuard)
@Post(':id/cards')
createCardInDeck(@Param('id') id: string, @Req() req, @Body() card: CreateCardDto) {
return this.commandBus.execute(new CreateCardCommand(req.user.id, id, card))
}
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Patch(':id') @Patch(':id')
update(@Param('id') id: string, @Body() updateDeckDto: UpdateDeckDto, @Req() req) { update(@Param('id') id: string, @Body() updateDeckDto: UpdateDeckDto, @Req() req) {

View File

@@ -8,8 +8,11 @@ import {
GetDeckByIdHandler, GetDeckByIdHandler,
GetAllDecksHandler, GetAllDecksHandler,
UpdateDeckHandler, UpdateDeckHandler,
GetAllCardsInDeckHandler,
} from './use-cases' } from './use-cases'
import { DecksRepository } from './infrastructure/decks.repository' import { DecksRepository } from './infrastructure/decks.repository'
import { CardsRepository } from '../cards/infrastructure/cards.repository'
import { CreateCardHandler } from './use-cases/create-card-use-case'
const commandHandlers = [ const commandHandlers = [
CreateDeckHandler, CreateDeckHandler,
@@ -17,12 +20,14 @@ const commandHandlers = [
GetDeckByIdHandler, GetDeckByIdHandler,
DeleteDeckByIdHandler, DeleteDeckByIdHandler,
UpdateDeckHandler, UpdateDeckHandler,
GetAllCardsInDeckHandler,
CreateCardHandler,
] ]
@Module({ @Module({
imports: [CqrsModule], imports: [CqrsModule],
controllers: [DecksController], controllers: [DecksController],
providers: [DecksService, DecksRepository, ...commandHandlers], providers: [DecksService, DecksRepository, CardsRepository, ...commandHandlers],
exports: [CqrsModule], exports: [CqrsModule],
}) })
export class DecksModule {} export class DecksModule {}

View File

@@ -0,0 +1,20 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { CreateCardDto } from '../../cards/dto/create-card.dto'
import { CardsRepository } from '../../cards/infrastructure/cards.repository'
export class CreateCardCommand {
constructor(
public readonly userId: string,
public readonly deckId: string,
public readonly card: CreateCardDto
) {}
}
@CommandHandler(CreateCardCommand)
export class CreateCardHandler implements ICommandHandler<CreateCardCommand> {
constructor(private readonly cardsRepository: CardsRepository) {}
async execute(command: CreateCardCommand) {
return await this.cardsRepository.createCard(command.deckId, command.userId, command.card)
}
}

View File

@@ -0,0 +1,28 @@
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'
export class GetAllCardsInDeckCommand {
constructor(
public readonly userId: string,
public readonly deckId: string,
public readonly params: GetAllCardsInDeckDto
) {}
}
@CommandHandler(GetAllCardsInDeckCommand)
export class GetAllCardsInDeckHandler implements ICommandHandler<GetAllCardsInDeckCommand> {
constructor(private readonly cardsRepository: CardsRepository) {}
async execute(command: GetAllCardsInDeckCommand) {
const deck = await this.cardsRepository.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`)
}
return await this.cardsRepository.findCardsByDeckId(command.deckId, command.params)
}
}

View File

@@ -3,3 +3,4 @@ export * from './get-all-decks-use-case'
export * from './get-deck-by-id-use-case' export * from './get-deck-by-id-use-case'
export * from './delete-deck-by-id-use-case' export * from './delete-deck-by-id-use-case'
export * from './update-deck-use-case' export * from './update-deck-use-case'
export * from './get-all-cards-in-deck-use-case'