Files
flashcards-api/src/modules/decks/infrastructure/decks.repository.ts
2024-03-12 16:21:13 +01:00

343 lines
8.8 KiB
TypeScript

import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
import { omit } from 'remeda'
import { PrismaService } from '../../../prisma.service'
import { DecksOrderBy, GetAllDecksDto } from '../dto'
import { Deck, PaginatedDecks } from '../entities/deck.entity'
@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
}): Promise<Deck> {
try {
const result = await this.prisma.deck.create({
data: {
author: {
connect: {
id: userId,
},
},
name,
cover,
isPrivate,
},
include: {
_count: {
select: {
card: true,
},
},
author: {
select: {
id: true,
name: true,
},
},
},
})
return { ...result, cardsCount: result._count.card }
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
async findMinMaxCards(): Promise<{ min: number; max: number }> {
const result = await this.prisma
.$queryRaw`SELECT MAX(card_count) as maxCardsCount, MIN(card_count) as minCardsCount FROM (SELECT deck.id, COUNT(card.id) as card_count FROM deck LEFT JOIN card ON deck.id = card."deckId" GROUP BY deck.id) AS card_counts;`
return {
max: Number(result[0].maxCardsCount),
min: Number(result[0].minCardsCount),
}
}
async findAllDecks({
name = undefined,
authorId = undefined,
userId,
currentPage,
itemsPerPage,
minCardsCount,
maxCardsCount,
orderBy,
}: GetAllDecksDto): Promise<PaginatedDecks> {
if (!orderBy || orderBy === 'null') {
orderBy = DecksOrderBy['updated-desc']
}
let orderField = 'd.updated' // default order field
let orderDirection = 'DESC' // default order direction
if (orderBy) {
const orderByParts = orderBy.split('-')
if (orderByParts.length === 2) {
const field = orderByParts[0]
const direction = orderByParts[1].toUpperCase()
// Validate the field and direction
if (
['cardsCount', 'updated', 'name', 'author.name', 'created'].includes(field) &&
['ASC', 'DESC'].includes(direction)
) {
if (field === 'cardsCount') {
orderField = 'cardsCount'
} else if (field === 'author.name') {
orderField = 'a.name'
} else {
orderField = `d.${field}`
}
orderDirection = direction
}
}
}
try {
// Prepare the where clause conditions
const conditions = []
if (name) conditions.push(`d.name LIKE ('%' || ? || '%')`)
if (authorId) conditions.push(`d."userId" = ?`)
if (userId)
conditions.push(`(d."isPrivate" = FALSE OR (d."isPrivate" = TRUE AND d."userId" = ?))`)
// Prepare the having clause for card count range
const havingConditions = []
if (minCardsCount != null) havingConditions.push(`COUNT(c.id) >= ?`)
if (maxCardsCount != null) havingConditions.push(`COUNT(c.id) <= ?`)
// Construct the raw SQL query for fetching decks
const query = `
SELECT
d.*,
COUNT(c.id) AS "cardsCount",
a."id" AS "authorId",
a."name" AS "authorName"
FROM deck AS "d"
LEFT JOIN "card" AS c ON d."id" = c."deckId"
LEFT JOIN "user" AS a ON d."userId" = a.id
${
conditions.length
? `WHERE ${conditions.map((_, index) => `${_.replace('?', `$${index + 1}`)}`).join(' AND ')}`
: ''
}
GROUP BY d."id", a."id"
${
havingConditions.length
? `HAVING ${havingConditions
.map((_, index) => `${_.replace('?', `$${conditions.length + index + 1}`)}`)
.join(' AND ')}`
: ''
}
ORDER BY ${orderField} ${orderDirection}
LIMIT $${conditions.length + havingConditions.length + 1} OFFSET $${
conditions.length + havingConditions.length + 2
};
`
// Parameters for fetching decks
const deckQueryParams = [
...(name ? [name] : []),
...(authorId ? [authorId] : []),
...(userId ? [userId] : []),
...(minCardsCount != null ? [minCardsCount] : []),
...(maxCardsCount != null ? [maxCardsCount] : []),
itemsPerPage,
(currentPage - 1) * itemsPerPage,
]
// Execute the raw SQL query for fetching decks
const decks = await this.prisma.$queryRawUnsafe<
Array<
Deck & {
authorId: string
authorName: string
}
>
>(query, ...deckQueryParams)
// Construct the raw SQL query for total count
const countQuery = `
SELECT COUNT(*) AS total
FROM (
SELECT d.id
FROM deck AS d
LEFT JOIN card AS c ON d.id = c."deckId"
${
conditions.length
? `WHERE ${conditions
.map((_, index) => `${_.replace('?', `$${index + 1}`)}`)
.join(' AND ')}`
: ''
}
GROUP BY d.id
${
havingConditions.length
? `HAVING ${havingConditions
.map((_, index) => `${_.replace('?', `$${conditions.length + index + 1}`)}`)
.join(' AND ')}`
: ''
}
) AS subquery;
`
// Parameters for total count query
const countQueryParams = [
...(name ? [name] : []),
...(authorId ? [authorId] : []),
...(userId ? [userId] : []),
...(minCardsCount != null ? [minCardsCount] : []),
...(maxCardsCount != null ? [maxCardsCount] : []),
]
// Execute the raw SQL query for total count
const totalResult = await this.prisma.$queryRawUnsafe<any[]>(countQuery, ...countQueryParams)
const total = Number(totalResult[0]?.total) ?? 1
const modifiedDecks = decks.map(deck => {
const cardsCount = deck.cardsCount
return omit(
{
...deck,
cardsCount: typeof cardsCount === 'bigint' ? Number(cardsCount) : cardsCount,
isPrivate: !!deck.isPrivate,
author: {
id: deck.authorId,
name: deck.authorName,
},
},
['authorId', 'authorName']
)
})
// Return the result with pagination data
return {
items: modifiedDecks,
pagination: {
totalItems: total,
currentPage,
itemsPerPage,
totalPages: Math.ceil(total / itemsPerPage),
},
}
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
public async findDeckById(id: string): Promise<Deck> {
try {
const result = await this.prisma.deck.findUnique({
where: {
id,
},
include: {
_count: {
select: {
card: true,
},
},
},
})
return { ...result, cardsCount: result._count.card }
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
public async findDeckByCardId(cardId: string): Promise<Deck> {
try {
const card = await this.prisma.card.findUnique({
where: {
id: cardId,
},
})
if (!card) return null
const result = await this.prisma.deck.findUnique({
include: {
_count: {
select: {
card: true,
},
},
},
where: {
id: card.deckId,
},
})
return { ...result, cardsCount: result._count.card }
} 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 }
): Promise<Deck> {
try {
const result = await this.prisma.deck.update({
where: {
id,
},
data,
include: {
_count: {
select: {
card: true,
},
},
author: {
select: {
id: true,
name: true,
},
},
},
})
return { ...result, cardsCount: result._count.card }
} catch (e) {
this.logger.error(e?.message)
throw new InternalServerErrorException(e?.message)
}
}
}