From 68942e904f2626fcea9079db84b6e1512c2ec73a Mon Sep 17 00:00:00 2001 From: andres Date: Wed, 12 Jul 2023 13:06:37 +0200 Subject: [PATCH] fix pagination and create pagination service --- prisma/schema.prisma | 81 ++++++++-------- .../common/helpers/set-count-key.ts | 8 ++ .../common/pagination/pagination.dto.ts | 2 +- .../common/pagination/pagination.service.ts | 46 +++++++-- src/modules/auth/dto/registration.dto.ts | 3 +- .../cards/infrastructure/cards.repository.ts | 56 ++++++----- src/modules/decks/decks.controller.ts | 6 +- src/modules/decks/dto/get-all-decks.dto.ts | 11 ++- .../decks/infrastructure/decks.repository.ts | 97 ++++++++++++------- src/modules/users/api/users.controller.ts | 5 +- src/modules/users/entities/user.entity.ts | 2 +- .../users/infrastructure/users.repository.ts | 30 +++--- src/modules/users/services/users.service.ts | 4 +- src/types/types.ts | 23 +++-- 14 files changed, 233 insertions(+), 141 deletions(-) create mode 100644 src/infrastructure/common/helpers/set-count-key.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 547a221..82d0b1e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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]) } diff --git a/src/infrastructure/common/helpers/set-count-key.ts b/src/infrastructure/common/helpers/set-count-key.ts new file mode 100644 index 0000000..8078dff --- /dev/null +++ b/src/infrastructure/common/helpers/set-count-key.ts @@ -0,0 +1,8 @@ +import { omit } from 'remeda' + +export const setCountKey = (key: K, newKey: L) => { + return >(obj: T) => { + obj[newKey] = obj['_count'][key] + return omit(obj, ['_count']) as Omit & { [P in L]: number } + } +} diff --git a/src/infrastructure/common/pagination/pagination.dto.ts b/src/infrastructure/common/pagination/pagination.dto.ts index 65468e1..8fa575b 100644 --- a/src/infrastructure/common/pagination/pagination.dto.ts +++ b/src/infrastructure/common/pagination/pagination.dto.ts @@ -10,5 +10,5 @@ export class PaginationDto { @Type(() => Number) @IsOptional() @IsNumber() - pageSize?: number + itemsPerPage?: number } diff --git a/src/infrastructure/common/pagination/pagination.service.ts b/src/infrastructure/common/pagination/pagination.service.ts index 088e8de..1c22fbd 100644 --- a/src/infrastructure/common/pagination/pagination.service.ts +++ b/src/infrastructure/common/pagination/pagination.service.ts @@ -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(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( + [count, items]: [number, T], + { + currentPage, + itemsPerPage, + }: { + currentPage: number + itemsPerPage: number + } + ) { + const totalPages = Math.ceil(count / itemsPerPage) + return { + pagination: { + totalPages, + currentPage, + itemsPerPage, + totalItems: count, + }, + items, + } } } diff --git a/src/modules/auth/dto/registration.dto.ts b/src/modules/auth/dto/registration.dto.ts index fe26293..26a065b 100644 --- a/src/modules/auth/dto/registration.dto.ts +++ b/src/modules/auth/dto/registration.dto.ts @@ -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 diff --git a/src/modules/cards/infrastructure/cards.repository.ts b/src/modules/cards/infrastructure/cards.repository.ts index eb410b9..310d036 100644 --- a/src/modules/cards/infrastructure/cards.repository.ts +++ b/src/modules/cards/infrastructure/cards.repository.ts @@ -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({ diff --git a/src/modules/decks/decks.controller.ts b/src/modules/decks/decks.controller.ts index 1c2c6b6..786a561 100644 --- a/src/modules/decks/decks.controller.ts +++ b/src/modules/decks/decks.controller.ts @@ -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) diff --git a/src/modules/decks/dto/get-all-decks.dto.ts b/src/modules/decks/dto/get-all-decks.dto.ts index 73103b7..a9aa592 100644 --- a/src/modules/decks/dto/get-all-decks.dto.ts +++ b/src/modules/decks/dto/get-all-decks.dto.ts @@ -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() diff --git a/src/modules/decks/infrastructure/decks.repository.ts b/src/modules/decks/infrastructure/decks.repository.ts index 78c02b2..e73df7b 100644 --- a/src/modules/decks/infrastructure/decks.repository.ts +++ b/src/modules/decks/infrastructure/decks.repository.ts @@ -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({ diff --git a/src/modules/users/api/users.controller.ts b/src/modules/users/api/users.controller.ts index 8fcedd5..548d224 100644 --- a/src/modules/users/api/users.controller.ts +++ b/src/modules/users/api/users.controller.ts @@ -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 } diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 9cef036..0ad0f87 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -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 diff --git a/src/modules/users/infrastructure/users.repository.ts b/src/modules/users/infrastructure/users.repository.ts index d72560c..aa64a27 100644 --- a/src/modules/users/infrastructure/users.repository.ts +++ b/src/modules/users/infrastructure/users.repository.ts @@ -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> { 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') { diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index 6fe6eac..64c0ebf 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -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) { diff --git a/src/types/types.ts b/src/types/types.ts index 8a5363d..97258c4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -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 = { - 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 +export type CreateUserInput = Omit 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'