diff --git a/src/modules/decks/decks.controller.ts b/src/modules/decks/decks.controller.ts index 144a88a..0b5fa12 100644 --- a/src/modules/decks/decks.controller.ts +++ b/src/modules/decks/decks.controller.ts @@ -50,6 +50,7 @@ import { GetDeckByIdCommand, GetMinMaxCardsUseCaseCommand, GetRandomCardInDeckCommand, + RemoveDeckFromFavoritesCommand, SaveGradeCommand, UpdateDeckCommand, } from './use-cases' @@ -261,4 +262,18 @@ export class DecksController { async addToFavorites(@Req() req, @Param('id') deckId: string): Promise { return await this.commandBus.execute(new AddDeckToFavoritesCommand(req.user.id, deckId)) } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiUnauthorizedResponse({ description: 'Unauthorized' }) + @HttpCode(HttpStatus.NO_CONTENT) + @ApiNoContentResponse({ description: 'Added to favorites' }) + @Delete(':id/favorite') + @ApiOperation({ + description: 'Add deck to favorites', + summary: 'Add deck to favorites', + }) + async removeFromFavorites(@Req() req, @Param('id') deckId: string): Promise { + return await this.commandBus.execute(new RemoveDeckFromFavoritesCommand(req.user.id, deckId)) + } } diff --git a/src/modules/decks/decks.module.ts b/src/modules/decks/decks.module.ts index 20fef37..3fbc2db 100644 --- a/src/modules/decks/decks.module.ts +++ b/src/modules/decks/decks.module.ts @@ -21,6 +21,7 @@ import { AddDeckToFavoritesHandler, SaveGradeHandler, GetRandomCardInDeckHandler, + RemoveDeckFromFavoritesHandler, } from './use-cases' const commandHandlers = [ @@ -36,6 +37,7 @@ const commandHandlers = [ CreateCardHandler, SaveGradeHandler, AddDeckToFavoritesHandler, + RemoveDeckFromFavoritesHandler, ] @Module({ diff --git a/src/modules/decks/infrastructure/decks.repository.ts b/src/modules/decks/infrastructure/decks.repository.ts index fdfd6bd..6a03774 100644 --- a/src/modules/decks/infrastructure/decks.repository.ts +++ b/src/modules/decks/infrastructure/decks.repository.ts @@ -129,38 +129,42 @@ export class DecksRepository { // 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 flashcards.deck AS "d" - LEFT JOIN "flashcards"."card" AS c ON d."id" = c."deckId" - LEFT JOIN "flashcards"."user" AS a ON d."userId" = a.id - LEFT JOIN "flashcards"."favoriteDeck" AS fd ON d."id" = fd."deckId" - ${ - 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 - }; - ` + SELECT + d.*, + COUNT(c.id) AS "cardsCount", + a."id" AS "authorId", + a."name" AS "authorName", + (fd."userId" IS NOT NULL) AS "isFavorite" + FROM flashcards.deck AS "d" + LEFT JOIN "flashcards"."card" AS c ON d."id" = c."deckId" + LEFT JOIN "flashcards"."user" AS a ON d."userId" = a.id + LEFT JOIN "flashcards"."favoriteDeck" AS fd ON d."id" = fd."deckId" AND fd."userId" = $1 + ${ + conditions.length + ? `WHERE ${conditions + .map((condition, index) => `${condition.replace('?', `$${index + 2}`)}`) + .join(' AND ')}` + : '' + } + GROUP BY d."id", a."id", fd."userId" + ${ + havingConditions.length + ? `HAVING ${havingConditions + .map( + (condition, index) => `${condition.replace('?', `$${conditions.length + index + 2}`)}` + ) + .join(' AND ')}` + : '' + } + ORDER BY ${orderField} ${orderDirection} + LIMIT $${conditions.length + havingConditions.length + 2} OFFSET $${ + conditions.length + havingConditions.length + 3 + }; + ` // Parameters for fetching decks const deckQueryParams = [ + userId, ...(name ? [name] : []), ...(authorId ? [authorId] : []), ...(userId ? [userId] : []), @@ -177,38 +181,43 @@ export class DecksRepository { Deck & { authorId: string authorName: string + isFavorite: boolean } > >(query, ...deckQueryParams) // Construct the raw SQL query for total count const countQuery = ` - SELECT COUNT(*) AS total - FROM ( - SELECT d.id - FROM flashcards.deck AS d - LEFT JOIN flashcards.card AS c ON d.id = c."deckId" - LEFT JOIN flashcards."favoriteDeck" AS fd ON d."id" = fd."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; - ` + SELECT COUNT(*) AS total + FROM ( + SELECT d.id + FROM flashcards.deck AS d + LEFT JOIN flashcards.card AS c ON d.id = c."deckId" + LEFT JOIN flashcards."favoriteDeck" AS fd ON d."id" = fd."deckId" AND fd."userId" = $1 + ${ + conditions.length + ? `WHERE ${conditions + .map((condition, index) => `${condition.replace('?', `$${index + 2}`)}`) + .join(' AND ')}` + : '' + } + GROUP BY d.id, fd."userId" + ${ + havingConditions.length + ? `HAVING ${havingConditions + .map( + (condition, index) => + `${condition.replace('?', `$${conditions.length + index + 2}`)}` + ) + .join(' AND ')}` + : '' + } + ) AS subquery; + ` // Parameters for total count query const countQueryParams = [ + userId, ...(name ? [name] : []), ...(authorId ? [authorId] : []), ...(userId ? [userId] : []), @@ -220,6 +229,7 @@ export class DecksRepository { // Execute the raw SQL query for total count const totalResult = await this.prisma.$queryRawUnsafe(countQuery, ...countQueryParams) const total = Number(totalResult[0]?.total) ?? 1 + const modifiedDecks = decks.map(deck => { const cardsCount = deck.cardsCount diff --git a/src/modules/decks/use-cases/add-card-to-favorites.use-case.ts b/src/modules/decks/use-cases/add-deck-to-favorites.use-case.ts similarity index 100% rename from src/modules/decks/use-cases/add-card-to-favorites.use-case.ts rename to src/modules/decks/use-cases/add-deck-to-favorites.use-case.ts diff --git a/src/modules/decks/use-cases/index.ts b/src/modules/decks/use-cases/index.ts index 5828c01..e38fb1f 100644 --- a/src/modules/decks/use-cases/index.ts +++ b/src/modules/decks/use-cases/index.ts @@ -9,4 +9,5 @@ export * from './get-random-card-in-deck-use-case' export * from './save-grade-use-case' export * from './update-deck-use-case' export * from './get-min-max-cards-use-case' -export * from './add-card-to-favorites.use-case' +export * from './add-deck-to-favorites.use-case' +export * from './remove-deck-from-favorites.use-case' diff --git a/src/modules/decks/use-cases/remove-deck-from-favorites.use-case.ts b/src/modules/decks/use-cases/remove-deck-from-favorites.use-case.ts new file mode 100644 index 0000000..c0cd590 --- /dev/null +++ b/src/modules/decks/use-cases/remove-deck-from-favorites.use-case.ts @@ -0,0 +1,36 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common' +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs' + +import { DecksRepository } from '../infrastructure/decks.repository' + +export class RemoveDeckFromFavoritesCommand { + constructor( + public readonly userId: string, + public readonly deckId: string + ) {} +} + +@CommandHandler(RemoveDeckFromFavoritesCommand) +export class RemoveDeckFromFavoritesHandler + implements ICommandHandler +{ + constructor(private readonly decksRepository: DecksRepository) {} + + async execute(command: RemoveDeckFromFavoritesCommand): Promise { + const deck = await this.decksRepository.findDeckById(command.deckId) + + if (!deck) { + throw new NotFoundException(`Deck with id ${command.deckId} not found`) + } + const favorites = await this.decksRepository.findFavoritesByUserId( + command.userId, + command.deckId + ) + + if (!favorites?.includes(command.deckId)) { + throw new BadRequestException(`Deck with id ${command.deckId} is not a favorite`) + } + + return await this.decksRepository.removeDeckFromFavorites(command.userId, command.deckId) + } +}