add decks crud

This commit is contained in:
2023-06-17 23:27:23 +02:00
parent 9cd6595ae2
commit 36e54cf56f
23 changed files with 405 additions and 35 deletions

View File

@@ -105,26 +105,20 @@ model Card {
}
model Deck {
id String @id @default(cuid())
userId String
userName String
name String
private Boolean
path String
grade Int
shots Int
cardsCount Int
deckCover String?
type String
rating Int
moreId String?
isDeleted Boolean?
isBlocked Boolean?
created DateTime @default(now())
updated DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
Card Card[] // One-to-many relation
Grade Grade[]
id String @id @default(cuid())
userId String
name String
isPrivate Boolean @default(false)
shots Int @default(0)
cover String?
rating Int @default(0)
isDeleted Boolean?
isBlocked Boolean?
created DateTime @default(now())
updated DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
Card Card[]
Grade Grade[]
@@index([userId])
}

View File

@@ -8,6 +8,7 @@ import { MailerModule } from '@nestjs-modules/mailer'
import * as process from 'process'
import { JwtRefreshStrategy } from './modules/auth/strategies/jwt-refresh.strategy'
import { CqrsModule } from '@nestjs/cqrs'
import { DecksModule } from './modules/decks/decks.module'
@Module({
imports: [
@@ -15,6 +16,7 @@ import { CqrsModule } from '@nestjs/cqrs'
ConfigModule,
UsersModule,
AuthModule,
DecksModule,
PrismaModule,
MailerModule.forRoot({

View File

@@ -0,0 +1,2 @@
export const DEFAULT_PAGE_SIZE = 10
export const DEFAULT_PAGE_NUMBER = 1

View File

@@ -0,0 +1,14 @@
import { IsNumber, IsOptional } from 'class-validator'
import { Type } from 'class-transformer'
export class PaginationDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
currentPage?: number
@Type(() => Number)
@IsOptional()
@IsNumber()
pageSize?: number
}

View File

@@ -0,0 +1,7 @@
import { ValidateIf, ValidationOptions } from 'class-validator'
export function IsOptionalOrEmptyString(validationOptions?: ValidationOptions) {
return ValidateIf((obj, value) => {
return value !== null && value !== undefined && value !== ''
}, validationOptions)
}

View File

@@ -1,9 +1,10 @@
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { BadRequestException, Logger, ValidationPipe } from '@nestjs/common'
import { Logger } from '@nestjs/common'
import { HttpExceptionFilter } from './exception.filter'
import * as cookieParser from 'cookie-parser'
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
import { pipesSetup } from './settings/pipes-setup'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
@@ -15,18 +16,7 @@ async function bootstrap() {
.build()
const document = SwaggerModule.createDocument(app, config)
SwaggerModule.setup('docs', app, document)
app.useGlobalPipes(
new ValidationPipe({
stopAtFirstError: false,
exceptionFactory: errors => {
const customErrors = errors.map(e => {
const firstError = JSON.stringify(e.constraints)
return { field: e.property, message: firstError }
})
throw new BadRequestException(customErrors)
},
})
)
pipesSetup(app)
app.useGlobalFilters(new HttpExceptionFilter())
app.use(cookieParser())
await app.listen(process.env.PORT || 3000)

View File

@@ -0,0 +1,60 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
Req,
Request,
UseGuards,
} from '@nestjs/common'
import { DecksService } from './decks.service'
import { CreateDeckDto } from './dto/create-deck.dto'
import { UpdateDeckDto } from './dto/update-deck.dto'
import { CommandBus } from '@nestjs/cqrs'
import { CreateDeckCommand } from './use-cases'
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
import { GetAllDecksCommand } from './use-cases/get-all-decks-use-case'
import { GetAllDecksDto } from './dto/get-all-decks.dto'
import { GetDeckByIdCommand } from './use-cases/get-deck-by-id-use-case'
import { DeleteDeckByIdCommand } from './use-cases/delete-deck-by-id-use-case'
import { UpdateDeckCommand } from './use-cases/update-deck-use-case'
@Controller('decks')
export class DecksController {
constructor(private readonly decksService: DecksService, private commandBus: CommandBus) {}
@UseGuards(JwtAuthGuard)
@Post()
create(@Request() req, @Body() createDeckDto: CreateDeckDto) {
const userId = req.user.id
return this.commandBus.execute(new CreateDeckCommand({ ...createDeckDto, userId: userId }))
}
@UseGuards(JwtAuthGuard)
@Get()
findAll(@Query() query: GetAllDecksDto, @Req() req) {
return this.commandBus.execute(new GetAllDecksCommand({ ...query, userId: req.user.id }))
}
@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: UpdateDeckDto, @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,26 @@
import { Module } from '@nestjs/common'
import { DecksService } from './decks.service'
import { DecksController } from './decks.controller'
import { CqrsModule } from '@nestjs/cqrs'
import { CreateDeckHandler } from './use-cases'
import { DecksRepository } from './infrastructure/decks.repository'
import { GetAllDecksHandler } from './use-cases/get-all-decks-use-case'
import { GetDeckByIdHandler } from './use-cases/get-deck-by-id-use-case'
import { DeleteDeckByIdHandler } from './use-cases/delete-deck-by-id-use-case'
import { UpdateDeckHandler } from './use-cases/update-deck-use-case'
const commandHandlers = [
CreateDeckHandler,
GetAllDecksHandler,
GetDeckByIdHandler,
DeleteDeckByIdHandler,
UpdateDeckHandler,
]
@Module({
imports: [CqrsModule],
controllers: [DecksController],
providers: [DecksService, DecksRepository, ...commandHandlers],
exports: [CqrsModule],
})
export class DecksModule {}

View File

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

View File

@@ -0,0 +1,16 @@
import { IsBoolean, IsOptional, IsString, Length } from 'class-validator'
export class CreateDeckDto {
@Length(3, 30)
name: string
@IsOptional()
@IsString()
cover?: string
@IsOptional()
@IsBoolean()
isPrivate?: boolean
userId: string
}

View File

@@ -0,0 +1,15 @@
import { IsOptional, IsUUID, Length } from 'class-validator'
import { IsOptionalOrEmptyString } from '../../../infrastructure/decorators/is-optional-or-empty-string'
import { PaginationDto } from '../../../infrastructure/common/pagination/pagination.dto'
export class GetAllDecksDto extends PaginationDto {
@IsOptional()
@Length(3, 30)
name?: string
@IsOptionalOrEmptyString()
@IsUUID(4)
authorId?: string
userId: string
}

View File

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

View File

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

View File

@@ -0,0 +1,126 @@
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
import { PrismaService } from '../../../prisma.service'
import { GetAllDecksDto } from '../dto/get-all-decks.dto'
import {
DEFAULT_PAGE_NUMBER,
DEFAULT_PAGE_SIZE,
} from '../../../infrastructure/common/pagination/pagination.constants'
@Injectable()
export class DecksRepository {
constructor(private prisma: PrismaService) {}
private readonly logger = new Logger(DecksRepository.name)
async createDeck({
name,
userId,
cover,
isPrivate,
}: {
name: string
userId: string
cover?: string
isPrivate?: boolean
}) {
try {
return await this.prisma.deck.create({
data: {
user: {
connect: {
id: userId,
},
},
name,
cover,
isPrivate,
},
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
async findAllDecks({
name = undefined,
authorId = undefined,
userId,
currentPage = DEFAULT_PAGE_NUMBER,
pageSize = DEFAULT_PAGE_SIZE,
}: GetAllDecksDto) {
try {
return await this.prisma.deck.findMany({
where: {
name: {
contains: name,
},
user: {
id: authorId || undefined,
},
OR: [
{
AND: [
{
isPrivate: true,
},
{
userId: userId,
},
],
},
{
isPrivate: false,
},
],
},
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,16 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { CreateDeckDto } from '../dto/create-deck.dto'
import { DecksRepository } from '../infrastructure/decks.repository'
export class CreateDeckCommand {
constructor(public readonly deck: CreateDeckDto) {}
}
@CommandHandler(CreateDeckCommand)
export class CreateDeckHandler implements ICommandHandler<CreateDeckCommand> {
constructor(private readonly deckRepository: DecksRepository) {}
async execute(command: CreateDeckCommand) {
return await this.deckRepository.createDeck(command.deck)
}
}

View File

@@ -0,0 +1,21 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { DecksRepository } from '../infrastructure/decks.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: DecksRepository) {}
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,16 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { DecksRepository } from '../infrastructure/decks.repository'
import { GetAllDecksDto } from '../dto/get-all-decks.dto'
export class GetAllDecksCommand {
constructor(public readonly params: GetAllDecksDto) {}
}
@CommandHandler(GetAllDecksCommand)
export class GetAllDecksHandler implements ICommandHandler<GetAllDecksCommand> {
constructor(private readonly deckRepository: DecksRepository) {}
async execute(command: GetAllDecksCommand) {
return await this.deckRepository.findAllDecks(command.params)
}
}

View File

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

View File

@@ -0,0 +1 @@
export * from './create-deck-use-case'

View File

@@ -0,0 +1,27 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'
import { DecksRepository } from '../infrastructure/decks.repository'
import { UpdateDeckDto } from '../dto/update-deck.dto'
import { BadRequestException, NotFoundException } from '@nestjs/common'
export class UpdateDeckCommand {
constructor(
public readonly deckId: string,
public readonly deck: UpdateDeckDto,
public readonly userId: string
) {}
}
@CommandHandler(UpdateDeckCommand)
export class UpdateDeckHandler implements ICommandHandler<UpdateDeckCommand> {
constructor(private readonly deckRepository: DecksRepository) {}
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

@@ -11,7 +11,7 @@ import {
} from '@nestjs/common'
import { UsersService } from '../services/users.service'
import { CreateUserDto } from '../dto/create-user.dto'
import { Pagination } from '../../../infrastructure/common/pagination.service'
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
import { BaseAuthGuard } from '../../auth/guards/base-auth.guard'
import { CommandBus } from '@nestjs/cqrs'
import { CreateUserCommand } from '../../auth/use-cases'

View File

@@ -20,6 +20,7 @@ export function pipesSetup(app: INestApplication) {
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
stopAtFirstError: true,
exceptionFactory: (errors: ValidationError[]) => {