Skip to content

Commit

Permalink
feat: [WIP] favorite quotes
Browse files Browse the repository at this point in the history
see: #1
  • Loading branch information
carlos3g committed Aug 27, 2024
1 parent c38a7c2 commit 06f8594
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 24 deletions.
14 changes: 7 additions & 7 deletions prisma/dbml/schema.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Table users {
tags tags [not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [not null]
userOnFavoritable tag_on_favoritable [not null]
userOnFavoritable user_on_favoritable [not null]
}

Table password_change_request {
Expand All @@ -35,7 +35,7 @@ Table quotes {
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [not null]
tagOnTaggable tag_on_taggable [not null]
userOnFavoritable tag_on_favoritable [not null]
userOnFavoritable user_on_favoritable [not null]
}

Table sources {
Expand Down Expand Up @@ -68,7 +68,7 @@ Table authors {
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [not null]
tagOnTaggable tag_on_taggable [not null]
userOnFavoritable tag_on_favoritable [not null]
userOnFavoritable user_on_favoritable [not null]
}

Table tags {
Expand All @@ -95,7 +95,7 @@ Table tag_on_taggable {
}
}

Table tag_on_favoritable {
Table user_on_favoritable {
user users [not null]
userId BigInt [not null]
author authors
Expand Down Expand Up @@ -137,8 +137,8 @@ Ref: tag_on_taggable.taggableId > authors.id

Ref: tag_on_taggable.taggableId > quotes.id

Ref: tag_on_favoritable.userId > users.id
Ref: user_on_favoritable.userId > users.id

Ref: tag_on_favoritable.favoritableId > authors.id
Ref: user_on_favoritable.favoritableId > authors.id

Ref: tag_on_favoritable.favoritableId > quotes.id
Ref: user_on_favoritable.favoritableId > quotes.id
10 changes: 5 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ model Quote {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tagOnTaggable TagOnTaggable[]
userOnFavoritable UserOnFavoritable[]
userOnFavoritable UserOnFavoritable[] @relation("UserOnQuote")
@@map("quotes")
}
Expand Down Expand Up @@ -89,7 +89,7 @@ model Author {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tagOnTaggable TagOnTaggable[]
userOnFavoritable UserOnFavoritable[]
userOnFavoritable UserOnFavoritable[] @relation("UserOnAuthor")
@@map("authors")
}
Expand Down Expand Up @@ -132,11 +132,11 @@ enum FavoritableType {
model UserOnFavoritable {
user User @relation(fields: [userId], references: [id])
userId BigInt @db.BigInt
author Author? @relation(fields: [favoritableId], references: [id], map: "author_favoritableId")
quote Quote? @relation(fields: [favoritableId], references: [id], map: "quote_favoritableId")
author Author? @relation("UserOnAuthor", fields: [favoritableId], references: [id], map: "author_favoritableId")
quote Quote? @relation("UserOnQuote", fields: [favoritableId], references: [id], map: "quote_favoritableId")
favoritableId BigInt @map("favoritable_id") @db.BigInt
favoritableType FavoritableType @map("favoritable_type")
@@unique([userId, favoritableId, favoritableType])
@@map("tag_on_favoritable")
@@map("user_on_favoritable")
}
3 changes: 3 additions & 0 deletions src/quote/contracts/quote-repository.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PaginatedResult } from '@app/lib/prisma/helpers/pagination';
import type {
QuoteRepositoryCreateInput,
QuoteRepositoryDeleteInput,
QuoteRepositoryFindManyFavoritedByUserInput,
QuoteRepositoryFindManyInput,
QuoteRepositoryFindManyPaginatedInput,
QuoteRepositoryFindUniqueOrThrowInput,
Expand All @@ -18,6 +19,8 @@ abstract class QuoteRepositoryContract {

public abstract findManyPaginated(input: QuoteRepositoryFindManyPaginatedInput): Promise<PaginatedResult<Quote>>;

public abstract findManyFavoritedByUser(input?: QuoteRepositoryFindManyFavoritedByUserInput): Promise<Quote[]>;

public abstract findMany(input?: QuoteRepositoryFindManyInput): Promise<Quote[]>;

public abstract delete(input: QuoteRepositoryDeleteInput): Promise<void>;
Expand Down
4 changes: 4 additions & 0 deletions src/quote/dtos/favorite-quote-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface FavoriteQuoteInput {
quoteUuid: string;
userUuid: string;
}
6 changes: 6 additions & 0 deletions src/quote/dtos/quote-repository-dtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface QuoteRepositoryFindManyInput {
};
}

export interface QuoteRepositoryFindManyFavoritedByUserInput {
where: {
userId: number;
};
}

export interface QuoteRepositoryFindManyPaginatedInput {
where?: {
authorId?: number;
Expand Down
24 changes: 23 additions & 1 deletion src/quote/quote.controller.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { AuthorRepositoryContract } from '@app/author/contracts/author-repository.contract';
import { QuoteRepositoryContract } from '@app/quote/contracts/quote-repository.contract';
import { UserRepositoryContract } from '@app/user/contracts/user-repository.contract';
import type { User } from '@app/user/entities/user.entity';
import { HttpStatus } from '@nestjs/common';
import { authorFactory, quoteFactory } from '@test/factories';
import { getAccessToken } from '@test/auth';
import { authorFactory, quoteFactory, userFactory } from '@test/factories';
import { app, server } from '@test/server';
import * as request from 'supertest';

let userRepository: UserRepositoryContract;
let quoteRepository: QuoteRepositoryContract;
let authorRepository: AuthorRepositoryContract;
let user: User;
let token: string;

beforeAll(() => {
userRepository = app.get<UserRepositoryContract>(UserRepositoryContract);
quoteRepository = app.get<QuoteRepositoryContract>(QuoteRepositoryContract);
authorRepository = app.get<AuthorRepositoryContract>(AuthorRepositoryContract);
});

beforeEach(async () => {
user = await userRepository.create(userFactory());
token = await getAccessToken(app, { email: user.email });
});

describe('(GET) /quotes', () => {
it('should be able to view quotes paginated', async () => {
await quoteRepository.create(quoteFactory());
Expand Down Expand Up @@ -99,3 +111,13 @@ describe('(GET) /quotes/:uuid', () => {
});
});
});

describe.skip('(GET) /quotes/:uuid/favorite', () => {
it('should be able to view a quote', async () => {
const quote = await quoteRepository.create(quoteFactory());

const response = await request(server).get(`/quotes/${quote.uuid}/favorite`).auth(token, { type: 'bearer' }).send();

expect(response.status).toBe(HttpStatus.OK);
});
});
21 changes: 13 additions & 8 deletions src/quote/quote.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { Public } from '@app/auth/decorators/public.decorator';
import { UserDecorator } from '@app/auth/decorators/user.decorator';
import { QuotePaginatedQuery } from '@app/quote/dtos/quote-paginated-query';
import { FavoriteQuoteUseCase } from '@app/quote/use-cases/favorite-quote.use-case';
import { GetOneQuoteUseCase } from '@app/quote/use-cases/get-one-quote.use-case';
import { ListQuotePaginatedUseCase } from '@app/quote/use-cases/list-quote-paginated.use-case';
import { Controller, Get, HttpCode, HttpStatus, Param, Query } from '@nestjs/common';
import { User } from '@app/user/entities/user.entity';
import { Controller, Get, HttpCode, HttpStatus, Param, Post, Query } from '@nestjs/common';
import { ApiBearerAuth } from '@nestjs/swagger';

@Controller('quotes')
export class QuoteController {
public constructor(
private readonly listQuotePaginatedUseCase: ListQuotePaginatedUseCase,
private readonly getOneQuoteUseCase: GetOneQuoteUseCase
private readonly getOneQuoteUseCase: GetOneQuoteUseCase,
private readonly favoriteQuoteUseCase: FavoriteQuoteUseCase
) {}

@Public()
Expand All @@ -25,10 +30,10 @@ export class QuoteController {
return this.getOneQuoteUseCase.handle({ uuid });
}

// @ApiBearerAuth()
// @Post(':uuid/favorite')
// @HttpCode(HttpStatus.OK)
// public async favorite(@Param('uuid') uuid: string) {
// //
// }
@ApiBearerAuth()
@Post(':uuid/favorite')
@HttpCode(HttpStatus.OK)
public async favorite(@Param('uuid') uuid: string, @UserDecorator() user: User) {
return this.favoriteQuoteUseCase.handle({ quoteUuid: uuid, userUuid: user.uuid });
}
}
5 changes: 4 additions & 1 deletion src/quote/quote.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { QuoteRepositoryContract } from '@app/quote/contracts/quote-repository.c
import { QuoteController } from '@app/quote/quote.controller';
import { PrismaQuoteRepository } from '@app/quote/repositories/prisma-quote.repository';
import { QuoteService } from '@app/quote/services/quote.service';
import { FavoriteQuoteUseCase } from '@app/quote/use-cases/favorite-quote.use-case';
import { GetOneQuoteUseCase } from '@app/quote/use-cases/get-one-quote.use-case';
import { ListQuotePaginatedUseCase } from '@app/quote/use-cases/list-quote-paginated.use-case';
import { UserModule } from '@app/user/user.module';
import { Module } from '@nestjs/common';

@Module({
imports: [PrismaModule, AuthorModule],
imports: [PrismaModule, AuthorModule, UserModule],
controllers: [QuoteController],
providers: [
{
Expand All @@ -19,6 +21,7 @@ import { Module } from '@nestjs/common';
QuoteService,
ListQuotePaginatedUseCase,
GetOneQuoteUseCase,
FavoriteQuoteUseCase,
],
exports: [QuoteRepositoryContract],
})
Expand Down
70 changes: 69 additions & 1 deletion src/quote/repositories/prisma-quote.repository.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,34 @@ import { PrismaQuoteRepository } from '@app/quote/repositories/prisma-quote.repo
import { faker } from '@faker-js/faker';
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { quoteFactory } from '@test/factories';
import { FavoritableType } from '@prisma/client';
import { quoteFactory, userFactory } from '@test/factories';
import { prisma } from '@test/server';
import * as _ from 'lodash';

const createQuotesFavorited = async (args: { userId: number; count: number }) => {
const { userId, count } = args;

const quotes = await prisma.quote.createManyAndReturn({
data: _.range(count).map(quoteFactory),
});

const promises = quotes.map((quote) =>
prisma.quote.update({
data: {
userOnFavoritable: {
create: { userId, favoritableType: FavoritableType.Quote },
},
},
where: { uuid: quote.uuid },
})
);

await Promise.all(promises);

return quotes;
};

describe('PrismaQuoteRepository', () => {
let quoteRepository: PrismaQuoteRepository;

Expand Down Expand Up @@ -79,6 +103,50 @@ describe('PrismaQuoteRepository', () => {
});
});

describe.skip('findManyFavoritedByUser', () => {
it('should find quotes favorited by a specific user', async () => {
const user = await prisma.user.create({
data: userFactory(),
});

await prisma.quote.createMany({
data: _.range(5).map(quoteFactory),
});

const quotes = await createQuotesFavorited({
userId: Number(user.id),
count: 5,
});

const result = await quoteRepository.findManyFavoritedByUser({
where: { userId: Number(user.id) },
});

const favoritedQuotesUuid = quotes.map((quote) => quote.uuid);

expect(result).toHaveLength(5);
result.forEach((quote) => {
expect(favoritedQuotesUuid).toContain(quote.uuid);
});
});

it('should return an empty array if the user has no favorited quotes', async () => {
const user = await prisma.user.create({
data: userFactory(),
});

await prisma.quote.createMany({
data: _.range(5).map(quoteFactory),
});

const result = await quoteRepository.findManyFavoritedByUser({
where: { userId: Number(user.id) },
});

expect(result).toEqual([]);
});
});

describe('findManyPaginated', () => {
it('should find many quotes paginated', async () => {
await prisma.quote.createMany({
Expand Down
5 changes: 5 additions & 0 deletions src/quote/repositories/prisma-quote.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { QuoteRepositoryContract } from '@app/quote/contracts/quote-reposit
import type {
QuoteRepositoryCreateInput,
QuoteRepositoryDeleteInput,
QuoteRepositoryFindManyFavoritedByUserInput,
QuoteRepositoryFindManyInput,
QuoteRepositoryFindManyPaginatedInput,
QuoteRepositoryFindUniqueOrThrowInput,
Expand Down Expand Up @@ -62,6 +63,10 @@ export class PrismaQuoteRepository implements QuoteRepositoryContract {
return entities.map(prismaQuoteToQuoteAdapter);
}

public findManyFavoritedByUser(input: QuoteRepositoryFindManyFavoritedByUserInput): Promise<Quote[]> {
throw new Error('Method not implemented.');
}

public async create(input: QuoteRepositoryCreateInput) {
const entity = await this.prismaManager.getClient().quote.create({
data: input,
Expand Down
6 changes: 5 additions & 1 deletion src/quote/services/quote.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class QuoteService {}
export class QuoteService {
public favorite(args: { userId: number; quoteId: number }): Promise<void> {
throw new Error('Method not implemented.');
}
}
33 changes: 33 additions & 0 deletions src/quote/use-cases/favorite-quote.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { QuoteRepositoryContract } from '@app/quote/contracts/quote-repository.contract';
import type { FavoriteQuoteInput } from '@app/quote/dtos/favorite-quote-input';
import { QuoteService } from '@app/quote/services/quote.service';
import type { UseCaseHandler } from '@app/shared/interfaces';
import { UserRepositoryContract } from '@app/user/contracts/user-repository.contract';
import { Injectable } from '@nestjs/common';

@Injectable()
export class FavoriteQuoteUseCase implements UseCaseHandler {
public constructor(
private readonly quoteRepository: QuoteRepositoryContract,
private readonly userRepository: UserRepositoryContract,
private readonly quoteService: QuoteService
) {}

public async handle(input: FavoriteQuoteInput): Promise<void> {
const { quoteUuid, userUuid } = input;

const user = await this.userRepository.findUniqueOrThrow({
where: {
uuid: userUuid,
},
});

const quote = await this.quoteRepository.findUniqueOrThrow({
where: {
uuid: quoteUuid,
},
});

return this.quoteService.favorite({ quoteId: quote.id, userId: user.id });
}
}

0 comments on commit 06f8594

Please sign in to comment.