fix pagination and create pagination service

This commit is contained in:
andres
2023-07-12 13:06:37 +02:00
parent 3db8bfb0f8
commit 68942e904f
14 changed files with 233 additions and 141 deletions

View File

@@ -9,19 +9,19 @@ generator client {
previewFeatures = ["fullTextSearch", "fullTextIndex"]
}
model Verification {
model verification {
id String @id @default(cuid())
userId String @unique
isEmailVerified Boolean @default(false)
verificationToken String? @unique @default(uuid())
verificationTokenExpiry DateTime?
verificationEmailsSent Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model User {
model user {
id String @id @default(uuid())
email String @unique
password String
@@ -34,51 +34,51 @@ model User {
deleteTime Int?
created DateTime @default(now())
updated DateTime @updatedAt
cards Card[]
decks Deck[]
grades Grade[]
cards card[]
decks deck[]
grades grade[]
generalChatMessages GeneralChatMessage[]
verification Verification?
RevokedToken RevokedToken[]
RefreshToken RefreshToken[]
ResetPassword ResetPassword?
verification verification?
revokedToken revokedToken[]
RefreshToken refreshToken[]
resetPassword resetPassword?
@@fulltext([name, email])
}
model RevokedToken {
model revokedToken {
id String @id @default(cuid())
userId String
token String @unique
revokedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model RefreshToken {
model refreshToken {
id String @id @default(cuid())
userId String
token String @unique @db.VarChar(255)
expiresAt DateTime
isRevoked Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model ResetPassword {
model resetPassword {
id String @id @default(cuid())
userId String @unique
resetPasswordToken String? @unique @default(uuid())
resetPasswordTokenExpiry DateTime?
resetPasswordEmailsSent Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model Card {
model card {
id String @id @default(cuid())
deckId String
userId String
@@ -96,34 +96,35 @@ model Card {
moreId String?
created DateTime @default(now())
updated DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
decks Deck @relation(fields: [deckId], references: [id], onDelete: Cascade)
grades Grade[]
author user @relation(fields: [userId], references: [id], onDelete: Cascade)
decks deck @relation(fields: [deckId], references: [id], onDelete: Cascade)
grades grade[]
@@index([userId])
@@index([deckId])
}
model Deck {
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[]
model deck {
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
author user @relation(fields: [userId], references: [id])
cardsCount Int @default(0)
card card[]
grade grade[]
@@index([userId])
}
model Grade {
model grade {
id String @id @default(cuid())
deckId String
cardId String
@@ -133,9 +134,9 @@ model Grade {
moreId String?
created DateTime @default(now())
updated DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
card Card @relation(fields: [cardId], references: [id])
decks Deck @relation(fields: [deckId], references: [id])
user user @relation(fields: [userId], references: [id])
card card @relation(fields: [cardId], references: [id])
decks deck @relation(fields: [deckId], references: [id])
@@index([userId])
@@index([deckId])
@@ -151,7 +152,7 @@ model GeneralChatMessage {
message String
created DateTime @default(now())
updated DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
user user @relation(fields: [userId], references: [id])
@@index([userId])
}

View File

@@ -0,0 +1,8 @@
import { omit } from 'remeda'
export const setCountKey = <K extends string, L extends string>(key: K, newKey: L) => {
return <T extends Record<string, any>>(obj: T) => {
obj[newKey] = obj['_count'][key]
return omit(obj, ['_count']) as Omit<T, '_count'> & { [P in L]: number }
}
}

View File

@@ -10,5 +10,5 @@ export class PaginationDto {
@Type(() => Number)
@IsOptional()
@IsNumber()
pageSize?: number
itemsPerPage?: number
}

View File

@@ -1,9 +1,43 @@
import { isObject } from 'remeda'
export class Pagination {
static getPaginationData(query) {
const page = typeof query.PageNumber === 'string' ? +query.PageNumber : 1
const pageSize = typeof query.PageSize === 'string' ? +query.PageSize : 10
const searchNameTerm = typeof query.SearchNameTerm === 'string' ? query.SearchNameTerm : ''
const searchEmailTerm = typeof query.SearchEmailTerm === 'string' ? query.SearchEmailTerm : ''
return { page, pageSize, searchNameTerm, searchEmailTerm }
static getPaginationData<T>(query: T) {
if (!isObject(query)) throw new Error('Pagination.getPaginationData: query is not an object')
const currentPage =
'currentPage' in query &&
typeof query.currentPage === 'string' &&
!isNaN(Number(query.currentPage))
? +query.currentPage
: 1
const itemsPerPage =
'itemsPerPage' in query &&
typeof query.itemsPerPage === 'string' &&
!isNaN(Number(query.itemsPerPage))
? +query.itemsPerPage
: 10
return { currentPage, itemsPerPage, ...query }
}
static transformPaginationData<T>(
[count, items]: [number, T],
{
currentPage,
itemsPerPage,
}: {
currentPage: number
itemsPerPage: number
}
) {
const totalPages = Math.ceil(count / itemsPerPage)
return {
pagination: {
totalPages,
currentPage,
itemsPerPage,
totalItems: count,
},
items,
}
}
}

View File

@@ -1,7 +1,8 @@
import { IsEmail, Length } from 'class-validator'
import { IsEmail, Length, IsOptional } from 'class-validator'
export class RegistrationDto {
@Length(3, 30)
@IsOptional()
name: string
@Length(3, 30)
password: string

View File

@@ -1,32 +1,44 @@
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,
return await this.prisma.$transaction(async tx => {
const created = await tx.card.create({
data: {
author: {
connect: {
id: userId,
},
},
decks: {
connect: {
id: deckId,
},
},
...card,
},
})
await tx.deck.update({
where: {
id: deckId,
},
data: {
cardsCount: {
increment: 1,
},
},
decks: {
connect: {
id: deckId,
},
},
...card,
},
})
return created
})
} catch (e) {
this.logger.error(e?.message)
@@ -36,12 +48,7 @@ export class CardsRepository {
async findCardsByDeckId(
deckId: string,
{
answer = undefined,
question = undefined,
currentPage = DEFAULT_PAGE_NUMBER,
pageSize = DEFAULT_PAGE_SIZE,
}: GetAllCardsInDeckDto
{ answer = undefined, question = undefined, currentPage, itemsPerPage }: GetAllCardsInDeckDto
) {
try {
return await this.prisma.card.findMany({
@@ -56,14 +63,15 @@ export class CardsRepository {
contains: answer || undefined,
},
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
skip: (currentPage - 1) * itemsPerPage,
take: itemsPerPage,
})
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
public async findDeckById(id: string) {
try {
return await this.prisma.deck.findUnique({

View File

@@ -26,8 +26,9 @@ import {
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/create-card-use-case'
import { CreateCardCommand } from './use-cases'
import { CreateCardDto } from '../cards/dto/create-card.dto'
import { Pagination } from '../../infrastructure/common/pagination/pagination.service'
@Controller('decks')
export class DecksController {
@@ -43,7 +44,8 @@ export class DecksController {
@UseGuards(JwtAuthGuard)
@Get()
findAll(@Query() query: GetAllDecksDto, @Req() req) {
return this.commandBus.execute(new GetAllDecksCommand({ ...query, userId: req.user.id }))
const finalQuery = Pagination.getPaginationData(query)
return this.commandBus.execute(new GetAllDecksCommand({ ...finalQuery, userId: req.user.id }))
}
@UseGuards(JwtAuthGuard)

View File

@@ -1,10 +1,15 @@
import { IsOptional, IsUUID, Length } from 'class-validator'
import { IsUUID } 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)
@IsOptionalOrEmptyString()
minCardsCount?: string
@IsOptionalOrEmptyString()
maxCardsCount?: string
@IsOptionalOrEmptyString()
name?: string
@IsOptionalOrEmptyString()

View File

@@ -1,15 +1,14 @@
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'
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
@Injectable()
export class DecksRepository {
constructor(private prisma: PrismaService) {}
private readonly logger = new Logger(DecksRepository.name)
async createDeck({
name,
userId,
@@ -24,7 +23,7 @@ export class DecksRepository {
try {
return await this.prisma.deck.create({
data: {
user: {
author: {
connect: {
id: userId,
},
@@ -45,42 +44,74 @@ export class DecksRepository {
name = undefined,
authorId = undefined,
userId,
currentPage = DEFAULT_PAGE_NUMBER,
pageSize = DEFAULT_PAGE_SIZE,
currentPage,
itemsPerPage,
minCardsCount,
maxCardsCount,
}: GetAllDecksDto) {
console.log({ name, authorId, userId, currentPage, itemsPerPage, minCardsCount, maxCardsCount })
try {
return await this.prisma.deck.findMany({
where: {
name: {
contains: name,
},
user: {
id: authorId || undefined,
},
OR: [
{
AND: [
{
isPrivate: true,
},
{
userId: userId,
},
],
},
{
isPrivate: false,
},
],
const where = {
cardsCount: {
gte: Number(minCardsCount) ?? undefined,
lte: Number(maxCardsCount) ?? undefined,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
name: {
contains: name,
},
author: {
id: authorId || undefined,
},
OR: [
{
AND: [
{
isPrivate: true,
},
{
userId: userId,
},
],
},
{
isPrivate: false,
},
],
}
const [count, items, max] = await this.prisma.$transaction([
this.prisma.deck.count({
where,
}),
this.prisma.deck.findMany({
where,
orderBy: {
created: 'desc',
},
include: {
author: {
select: {
id: true,
name: true,
},
},
},
skip: (currentPage - 1) * itemsPerPage,
take: itemsPerPage,
}),
this.prisma
.$queryRaw`SELECT MAX(card_count) as maxCardsCount FROM (SELECT COUNT(*) as card_count FROM card GROUP BY deckId) AS card_counts;`,
])
return {
maxCardsCount: Number(max[0].maxCardsCount),
...Pagination.transformPaginationData([count, items], { currentPage, itemsPerPage }),
}
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
public async findDeckById(id: string) {
try {
return await this.prisma.deck.findUnique({

View File

@@ -22,8 +22,9 @@ export class UsersController {
@Get()
async findAll(@Query() query) {
const { page, pageSize, searchNameTerm, searchEmailTerm } = Pagination.getPaginationData(query)
const users = await this.usersService.getUsers(page, pageSize, searchNameTerm, searchEmailTerm)
const { page, pageSize } = Pagination.getPaginationData(query)
const users = await this.usersService.getUsers(page, pageSize, query.name, query.email)
if (!users) throw new NotFoundException('Users not found')
return users
}

View File

@@ -1,6 +1,6 @@
import { Prisma } from '@prisma/client'
export class User implements Prisma.UserUncheckedCreateInput {
export class User implements Prisma.userUncheckedCreateInput {
id: string
email: string
password: string

View File

@@ -14,8 +14,8 @@ import {
import { addHours } from 'date-fns'
import { v4 as uuidv4 } from 'uuid'
import { PrismaService } from '../../../prisma.service'
import { pick } from 'remeda'
import { Prisma } from '@prisma/client'
import { Pagination } from '../../../infrastructure/common/pagination/pagination.service'
@Injectable()
export class UsersRepository {
@@ -30,31 +30,29 @@ export class UsersRepository {
searchEmailTerm: string
): Promise<EntityWithPaginationType<UserViewType>> {
try {
const where = {
const where: Prisma.userWhereInput = {
name: {
search: searchNameTerm || undefined,
contains: searchNameTerm || undefined,
},
email: {
search: searchEmailTerm || undefined,
contains: searchEmailTerm || undefined,
},
}
const [totalItems, users] = await this.prisma.$transaction([
const res = await this.prisma.$transaction([
this.prisma.user.count({ where }),
this.prisma.user.findMany({
where,
select: {
id: true,
name: true,
email: true,
isEmailVerified: true,
},
skip: (currentPage - 1) * itemsPerPage,
take: itemsPerPage,
}),
])
const totalPages = Math.ceil(totalItems / itemsPerPage)
const usersView = users.map(u => pick(u, ['id', 'name', 'email', 'isEmailVerified']))
return {
totalPages,
currentPage,
itemsPerPage,
totalItems,
items: usersView,
}
return Pagination.transformPaginationData(res, { currentPage, itemsPerPage })
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === 'P2025') {
@@ -124,7 +122,7 @@ export class UsersRepository {
}
}
async findUserById(id: string, include?: Prisma.UserInclude) {
async findUserById(id: string, include?: Prisma.userInclude) {
try {
const user = await this.prisma.user.findUnique({
where: { id },
@@ -134,7 +132,7 @@ export class UsersRepository {
return null
}
return user as Prisma.UserGetPayload<{ include: typeof include }>
return user as Prisma.userGetPayload<{ include: typeof include }>
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === 'P2015') {

View File

@@ -9,8 +9,8 @@ export class UsersService {
private logger = new Logger(UsersService.name)
async getUsers(page: number, pageSize: number, searchNameTerm: string, searchEmailTerm: string) {
return await this.usersRepository.getUsers(page, pageSize, searchNameTerm, searchEmailTerm)
async getUsers(page: number, pageSize: number, name: string, email: string) {
return await this.usersRepository.getUsers(page, pageSize, name, email)
}
async getUserById(id: string) {

View File

@@ -1,4 +1,5 @@
import { Prisma } from '@prisma/client'
export type NewestLikesType = {
id: string
login: string
@@ -46,12 +47,13 @@ export type CommentType = {
myStatus: string
}
}
export type EntityWithPaginationType<T> = {
totalPages: number
currentPage: number
itemsPerPage: number
totalItems: number
pagination: {
totalPages: number
currentPage: number
itemsPerPage: number
totalItems: number
}
items: T[]
}
@@ -65,22 +67,22 @@ export type ErrorMessageType = {
field: string
}
const userInclude: Prisma.UserInclude = {
const userInclude: Prisma.userInclude = {
verification: true,
}
export type VerificationWithUser = Prisma.VerificationGetPayload<{
export type VerificationWithUser = Prisma.verificationGetPayload<{
include: { user: true }
}>
export type User = Prisma.UserGetPayload<{
export type User = Prisma.userGetPayload<{
include: typeof userInclude
}>
export type CreateUserInput = Omit<Prisma.UserCreateInput & Prisma.VerificationCreateInput, 'user'>
export type CreateUserInput = Omit<Prisma.userCreateInput & Prisma.verificationCreateInput, 'user'>
export type UserType = {
accountData: Prisma.UserCreateInput
accountData: Prisma.userCreateInput
emailConfirmation: EmailConfirmationType
}
@@ -139,4 +141,5 @@ export enum LikeAction {
Dislike = 'Dislike',
None = 'None',
}
export type LikeActionType = 'Like' | 'Dislike' | 'None'