diff --git a/.claude/agents/http-test-syncer.md b/.claude/agents/http-test-syncer.md new file mode 100644 index 0000000..e2093ac --- /dev/null +++ b/.claude/agents/http-test-syncer.md @@ -0,0 +1,25 @@ +--- +name: http-test-syncer +description: Syncs test/http/trackgeek-api.http with the real controller routes and DTOs — adds/updates numbered request blocks so the manual .http file matches the API. Use after adding or changing endpoints, or when the .http file looks stale. +tools: Read, Write, Edit, Grep, Glob +--- + +You keep `test/http/trackgeek-api.http` in sync with the actual controllers. + +## File format (preserve exactly) +- Top has `### Variables` with `@apiUrl = http://localhost:40287`. +- Auth: there is a named login request `# @name login` (`POST {{apiUrl}}/auth/sign-in/email`). Reuse its response: `{{login.response.body.user.id}}`, `{{login.response.body.user.username}}`. +- Each request is a numbered `### N. ` block, monotonically increasing. Sub-steps use `### N.1`, `### N.2`. +- Method + URL on next line, then headers (`Content-Type: application/json`), blank line, then JSON body for mutations. + +## Steps +1. Read `test/http/trackgeek-api.http` to learn current numbering and style. +2. For the target controller(s), read the controller file(s) to extract: HTTP method, full path (`@Controller` prefix + method path), required guards, and the DTO (query vs body) with its fields/types. +3. For each missing or changed route, add/update a numbered block: + - Auth-guarded routes assume the `login` request ran first; no manual token needed if cookies are used — match how existing guarded blocks are written. + - Build a realistic JSON body from the DTO fields (use the `@ApiProperty` `example`/`default` when present). + - Use `{{login.response.body...}}` placeholders for ids belonging to the logged-in user. +4. Keep numbering consistent — append new blocks; renumber only if the user asks. +5. Report which routes you added vs which already existed. + +Do not invent routes. Only emit blocks that correspond to a real controller method. If a DTO field is ambiguous, use a clearly-fake placeholder and flag it. diff --git a/.claude/agents/integration-mapper.md b/.claude/agents/integration-mapper.md new file mode 100644 index 0000000..b630e6c --- /dev/null +++ b/.claude/agents/integration-mapper.md @@ -0,0 +1,35 @@ +--- +name: integration-mapper +description: Maps a new external API endpoint into a TrackGeek integration service (TMDB, IGDB, Jikan, Hardcover) — adds the typed method, request DTO, and enums following the existing mapping style. Use when adding a new external data source call or field mapping. +tools: Read, Write, Edit, Grep, Glob, Bash +--- + +You add or extend external-API mappings in the TrackGeek integration layer. + +## Layout +`src/shared/infra/integrations/` +- `tmdb.service.ts` — movies + tv shows +- `igdb.service.ts` — games +- `jikan.service.ts` — anime + manga (MyAnimeList) +- `hardcover.service.ts` — books +- `imgbb.service.ts` — image upload +- `integrations.service.ts` — facade exposing `.tmdb`, `.igdb`, `.jikan`, `.hardcover`, `.imgbb` + +Consumers call `this.integrationsService..(dto)`. + +## Convention (read the target service first — these files are large, 30-40K) +- Each provider is an `@Injectable()` service. +- Methods return normalized domain shapes, NOT raw API JSON. Map field-by-field to the internal shape (e.g. raw `id` → `tmdbId`). Match the naming already used in sibling methods. +- Paginated endpoints return a `{ items, ...pagination }` shape — copy the existing pagination type used by `searchMovies`/`topMovies`. +- Enums for ordering/sort/filter live IN the service file and are exported (e.g. `TMDBMovieOrderBy`, `TMDBSort`), then imported by DTOs. +- Request DTOs that wrap these calls live in the consuming module's `dto/`, using `class-validator` + `@ApiProperty(Optional)`. + +## Steps +1. Read the target provider service fully; find the closest existing method and mirror its structure, error handling, and normalization. +2. Add the new method with a precise return type (define an interface/type next to siblings). +3. Add/extend enums in the service and export them. +4. If a consuming DTO is needed, add it under the relevant `src/modules//dto/`. +5. Verify types: `bun run build` or `tsc --noEmit` (check package.json scripts first). +6. Report the request/response field mapping table so the user can confirm field names. + +Never dump raw upstream JSON through the controller. Always normalize. Quote the upstream endpoint/path you mapped against. diff --git a/.claude/agents/module-scaffolder.md b/.claude/agents/module-scaffolder.md new file mode 100644 index 0000000..0cb38a7 --- /dev/null +++ b/.claude/agents/module-scaffolder.md @@ -0,0 +1,64 @@ +--- +name: module-scaffolder +description: Scaffolds a new NestJS feature module (dto + controller + service + module) following the TrackGeek convention, and registers it in app.module.ts. Use when the user asks to create a new module/domain/resource. +tools: Read, Write, Edit, Grep, Glob +--- + +You scaffold a new feature module for the TrackGeek API (NestJS + Prisma + Redis). + +## Reference convention (copy exactly) + +Study `src/modules/movie/` before generating. The shape is non-negotiable: + +``` +src/modules// + .module.ts + dto/-.dto.ts # one DTO per action + controller/.controller.ts # + sub-controllers like -review.controller.ts + service/.service.ts # + matching sub-services +``` + +### Module file +```ts +@Module({ + imports: [], + controllers: [/* all controllers */], + providers: [/* all services */], + exports: [/* ALL services — always exported */], +}) +export class Module {} +``` + +### Controller +- `@ApiTags("")`, `@Controller("/")` +- Methods return a wrapped object: `return { movies }`, never the bare value +- Mutations (`@Post`/`@Patch`/`@Delete`) get `@UseGuards(AuthGuard)` from `@thallesp/nestjs-better-auth` +- Numeric params: `@Param("id", new ParseIntPipe())` +- Query DTOs via `@Query()`, body DTOs via `@Body()` + +### Service +- `@Injectable()`, constructor injects what it needs from: + `CacheService` (`@/shared/infra/cache/cache.service`), + `DatabaseService` (`@/shared/infra/database/database.service`), + `IntegrationsService` (`@/shared/infra/integrations/integrations.service`) +- Errors: `throw new AppException(ERROR_CODES.X)` from `@/shared/exceptions/app.exceptions` + `@/shared/constants/error-codes` +- Cache keys from `@/shared/constants/cache` (`CACHE_KEYS`) +- Prisma types from `@prisma/generated/client` and `@prisma/generated/models` + +### DTO +- `class-validator` decorators (`@IsOptional`, `@IsString`, `@IsInt`, `@IsEnum`, `@IsPositive`, `@IsArray`) +- `@Type(() => Number)` / `@Transform(...)` from `class-transformer` for coercion +- `@ApiProperty` (required) / `@ApiPropertyOptional` (optional) on every field +- Fields are `readonly` + +### Path alias +`@/*` → `src/*`. Prefer `@/...` for cross-module imports, relative `../` within the module. + +## Steps +1. Read `src/modules/movie/` files as the template. +2. Confirm the domain + which sub-resources (e.g. progress, review) and their endpoints with the user if unclear. +3. Generate every file. No placeholder TODOs in boilerplate — wire real Prisma calls where the model exists in `prisma/schema.prisma`. +4. Register the module in `src/app.module.ts`: add the import (alphabetical with siblings) and add to the `imports` array. +5. Report the created file tree and any DTO fields you guessed so the user can correct them. + +Do NOT invent Prisma models. If the schema lacks the model, say so and stop before writing service DB logic. diff --git a/.claude/agents/swagger-auditor.md b/.claude/agents/swagger-auditor.md new file mode 100644 index 0000000..989e68c --- /dev/null +++ b/.claude/agents/swagger-auditor.md @@ -0,0 +1,25 @@ +--- +name: swagger-auditor +description: Audits @nestjs/swagger usage across a module — finds DTO fields missing @ApiProperty/@ApiPropertyOptional, controllers missing @ApiTags, enum/type mismatches, and required-vs-optional drift between class-validator and Swagger decorators. Read-only reporter. Use to check API docs coverage. +tools: Read, Grep, Glob +--- + +You audit Swagger/OpenAPI decorator coverage. Docs are served via Scalar + `@nestjs/swagger`, so they are only as good as the decorators. Read-only — you report, you do NOT edit. + +## Rules to enforce +1. **Every controller** has `@ApiTags("")`. +2. **Every DTO field** has `@ApiProperty` (for required) or `@ApiPropertyOptional` (for optional). +3. **Required/optional consistency**: a field with `@IsOptional()` should use `@ApiPropertyOptional` (or `@ApiProperty({ required: false })`), and vice-versa. Flag mismatches. +4. **Enum fields**: `@IsEnum(X)` should carry `@ApiProperty({ enum: X })` / `@ApiPropertyOptional({ enum: X })`. +5. **Coerced fields**: fields with `@Type(() => Number)` should declare `type: "number"` in the Swagger decorator. +6. **Auth**: `@UseGuards(AuthGuard)` routes ideally carry `@ApiBearerAuth()`/`@ApiCookieAuth()` — flag if a project-wide convention exists and a route deviates. + +## Steps +1. Glob the target scope (a module under `src/modules//`, or all modules). +2. Read controllers and DTOs; for each, check rules 1-6. +3. Output a findings table, one row per issue: + `path:line: : . .` + Severities: 🔴 missing decorator, 🟡 mismatch/inconsistency, 🔵 nice-to-have. +4. End with a one-line coverage summary (e.g. "12 DTOs, 3 fields missing @ApiProperty"). + +No praise, no scope creep, no edits. If a module is fully compliant, say so in one line. diff --git a/package.json b/package.json index 1fb28d6..9cf9e67 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:seed": "prisma db seed", - "db:run": "npm run prisma:migrate && npm run prisma:generate && npm run prisma:seed", + "stripe:webhook": "stripe listen --forward-to localhost:40287/stripe/webhook", "test:unit": "vitest run --printConsoleTrace=true --silent=false", "test:unit:watch": "vitest", "test:unit:ui": "vitest --ui", diff --git a/prisma/migrations/20260422231629_init/migration.sql b/prisma/migrations/20260707215819_init/migration.sql similarity index 93% rename from prisma/migrations/20260422231629_init/migration.sql rename to prisma/migrations/20260707215819_init/migration.sql index 892d4c1..079637f 100644 --- a/prisma/migrations/20260422231629_init/migration.sql +++ b/prisma/migrations/20260707215819_init/migration.sql @@ -8,7 +8,7 @@ CREATE TYPE "UserTier" AS ENUM ('Tracker', 'Archivist', 'ArchiveMaster'); CREATE TYPE "CommentType" AS ENUM ('Anime', 'Manga', 'TVShow', 'Movie', 'Game', 'Book', 'Profile'); -- CreateEnum -CREATE TYPE "ReactionType" AS ENUM ('Comment', 'FeedEvent'); +CREATE TYPE "ReactionType" AS ENUM ('Comment', 'FeedEvent', 'GameReview', 'AnimeReview', 'MangaReview', 'TvShowReview', 'MovieReview', 'BookReview'); -- CreateEnum CREATE TYPE "FeedEventType" AS ENUM ('NewFollower', 'NewFavorite', 'NewList', 'NewListItem', 'NewReview', 'NewWatch', 'NewProgress'); @@ -172,6 +172,12 @@ CREATE TABLE "Reaction" ( "userId" TEXT NOT NULL, "commentId" TEXT, "feedEventId" TEXT, + "gameReviewId" TEXT, + "animeReviewId" TEXT, + "mangaReviewId" TEXT, + "tvShowReviewId" TEXT, + "movieReviewId" TEXT, + "bookReviewId" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "Reaction_pkey" PRIMARY KEY ("id") @@ -521,6 +527,7 @@ CREATE TABLE "TVShowProgress" ( "id" TEXT NOT NULL, "status" "ProgressStatus" NOT NULL, "watchCount" INTEGER, + "notes" TEXT, "startedAt" TIMESTAMP(3), "completedAt" TIMESTAMP(3), "userId" TEXT NOT NULL, @@ -811,25 +818,25 @@ CREATE INDEX "Following_followingId_idx" ON "Following"("followingId"); CREATE UNIQUE INDEX "Following_followerId_followingId_key" ON "Following"("followerId", "followingId"); -- CreateIndex -CREATE UNIQUE INDEX "Comment_userId_animeId_key" ON "Comment"("userId", "animeId"); +CREATE INDEX "Comment_animeId_idx" ON "Comment"("animeId"); -- CreateIndex -CREATE UNIQUE INDEX "Comment_userId_mangaId_key" ON "Comment"("userId", "mangaId"); +CREATE INDEX "Comment_mangaId_idx" ON "Comment"("mangaId"); -- CreateIndex -CREATE UNIQUE INDEX "Comment_userId_tvShowId_key" ON "Comment"("userId", "tvShowId"); +CREATE INDEX "Comment_tvShowId_idx" ON "Comment"("tvShowId"); -- CreateIndex -CREATE UNIQUE INDEX "Comment_userId_movieId_key" ON "Comment"("userId", "movieId"); +CREATE INDEX "Comment_movieId_idx" ON "Comment"("movieId"); -- CreateIndex -CREATE UNIQUE INDEX "Comment_userId_gameId_key" ON "Comment"("userId", "gameId"); +CREATE INDEX "Comment_gameId_idx" ON "Comment"("gameId"); -- CreateIndex -CREATE UNIQUE INDEX "Comment_userId_bookId_key" ON "Comment"("userId", "bookId"); +CREATE INDEX "Comment_bookId_idx" ON "Comment"("bookId"); -- CreateIndex -CREATE UNIQUE INDEX "Comment_userId_profileId_key" ON "Comment"("userId", "profileId"); +CREATE INDEX "Comment_profileId_idx" ON "Comment"("profileId"); -- CreateIndex CREATE UNIQUE INDEX "Reaction_userId_commentId_key" ON "Reaction"("userId", "commentId"); @@ -837,6 +844,24 @@ CREATE UNIQUE INDEX "Reaction_userId_commentId_key" ON "Reaction"("userId", "com -- CreateIndex CREATE UNIQUE INDEX "Reaction_userId_feedEventId_key" ON "Reaction"("userId", "feedEventId"); +-- CreateIndex +CREATE UNIQUE INDEX "Reaction_userId_gameReviewId_key" ON "Reaction"("userId", "gameReviewId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reaction_userId_animeReviewId_key" ON "Reaction"("userId", "animeReviewId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reaction_userId_mangaReviewId_key" ON "Reaction"("userId", "mangaReviewId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reaction_userId_tvShowReviewId_key" ON "Reaction"("userId", "tvShowReviewId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reaction_userId_movieReviewId_key" ON "Reaction"("userId", "movieReviewId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reaction_userId_bookReviewId_key" ON "Reaction"("userId", "bookReviewId"); + -- CreateIndex CREATE INDEX "FeedEvent_userId_idx" ON "FeedEvent"("userId"); @@ -1014,6 +1039,24 @@ ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_commentId_fkey" FOREIGN KEY ("co -- AddForeignKey ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_feedEventId_fkey" FOREIGN KEY ("feedEventId") REFERENCES "FeedEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_gameReviewId_fkey" FOREIGN KEY ("gameReviewId") REFERENCES "GameReview"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_animeReviewId_fkey" FOREIGN KEY ("animeReviewId") REFERENCES "AnimeReview"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_mangaReviewId_fkey" FOREIGN KEY ("mangaReviewId") REFERENCES "MangaReview"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_tvShowReviewId_fkey" FOREIGN KEY ("tvShowReviewId") REFERENCES "TVShowReview"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_movieReviewId_fkey" FOREIGN KEY ("movieReviewId") REFERENCES "MovieReview"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_bookReviewId_fkey" FOREIGN KEY ("bookReviewId") REFERENCES "BookReview"("id") ON DELETE CASCADE ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "FeedEvent" ADD CONSTRAINT "FeedEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5833a0e..e815a6a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -41,8 +41,8 @@ model User { accounts Account[] comments Comment[] reactions Reaction[] - followers Following[] @relation("UserFollowers") - following Following[] @relation("UserFollowing") + followers Following[] @relation("UserFollowing") + following Following[] @relation("UserFollowers") feedEvents FeedEvent[] animeEpisodesWatches AnimeEpisodeWatch[] lists List[] @@ -204,35 +204,59 @@ model Comment { profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade) reactions Reaction[] - @@unique([userId, animeId]) - @@unique([userId, mangaId]) - @@unique([userId, tvShowId]) - @@unique([userId, movieId]) - @@unique([userId, gameId]) - @@unique([userId, bookId]) - @@unique([userId, profileId]) + @@index([animeId]) + @@index([mangaId]) + @@index([tvShowId]) + @@index([movieId]) + @@index([gameId]) + @@index([bookId]) + @@index([profileId]) } enum ReactionType { Comment FeedEvent + GameReview + AnimeReview + MangaReview + TvShowReview + MovieReview + BookReview } model Reaction { - id String @id @default(uuid(7)) - emoji String - type ReactionType - userId String - commentId String? - feedEventId String? - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) - feedEvent FeedEvent? @relation(fields: [feedEventId], references: [id], onDelete: Cascade) + id String @id @default(uuid(7)) + emoji String + type ReactionType + userId String + commentId String? + feedEventId String? + gameReviewId String? + animeReviewId String? + mangaReviewId String? + tvShowReviewId String? + movieReviewId String? + bookReviewId String? + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) + feedEvent FeedEvent? @relation(fields: [feedEventId], references: [id], onDelete: Cascade) + gameReview GameReview? @relation(fields: [gameReviewId], references: [id], onDelete: Cascade) + animeReview AnimeReview? @relation(fields: [animeReviewId], references: [id], onDelete: Cascade) + mangaReview MangaReview? @relation(fields: [mangaReviewId], references: [id], onDelete: Cascade) + tvShowReview TvShowReview? @relation(fields: [tvShowReviewId], references: [id], onDelete: Cascade) + movieReview MovieReview? @relation(fields: [movieReviewId], references: [id], onDelete: Cascade) + bookReview BookReview? @relation(fields: [bookReviewId], references: [id], onDelete: Cascade) @@unique([userId, commentId]) @@unique([userId, feedEventId]) + @@unique([userId, gameReviewId]) + @@unique([userId, animeReviewId]) + @@unique([userId, mangaReviewId]) + @@unique([userId, tvShowReviewId]) + @@unique([userId, movieReviewId]) + @@unique([userId, bookReviewId]) } enum FeedEventType { @@ -644,6 +668,7 @@ model TvShowProgress { id String @id @default(uuid(7)) status ProgressStatus watchCount Int? + notes String? startedAt DateTime? completedAt DateTime? userId String @@ -728,8 +753,9 @@ model AnimeReview { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - anime Anime @relation(fields: [animeId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + anime Anime @relation(fields: [animeId], references: [id], onDelete: Cascade) + reactions Reaction[] @@unique([userId, animeId]) } @@ -749,8 +775,9 @@ model MangaReview { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - manga Manga @relation(fields: [mangaId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + manga Manga @relation(fields: [mangaId], references: [id], onDelete: Cascade) + reactions Reaction[] @@unique([userId, mangaId]) } @@ -770,8 +797,9 @@ model TvShowReview { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - tvShow TvShow @relation(fields: [tvShowId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + tvShow TvShow @relation(fields: [tvShowId], references: [id], onDelete: Cascade) + reactions Reaction[] @@unique([userId, tvShowId]) @@map("TVShowReview") @@ -792,8 +820,9 @@ model MovieReview { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - movie Movie @relation(fields: [movieId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + movie Movie @relation(fields: [movieId], references: [id], onDelete: Cascade) + reactions Reaction[] @@unique([userId, movieId]) } @@ -817,6 +846,7 @@ model GameReview { user User @relation(fields: [userId], references: [id], onDelete: Cascade) game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) gameReviewScreenshots GameReviewScreenshot[] + reactions Reaction[] @@unique([userId, gameId]) } @@ -849,8 +879,9 @@ model BookReview { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - book Book @relation(fields: [bookId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + book Book @relation(fields: [bookId], references: [id], onDelete: Cascade) + reactions Reaction[] @@unique([userId, bookId]) } diff --git a/prisma/seed.ts b/prisma/seed.ts index 9c43569..73774e6 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,15 +1,21 @@ import "dotenv/config"; -import { Pool } from "pg"; import { PrismaPg } from "@prisma/adapter-pg"; +import { Pool } from "pg"; import { v7 as uuid } from "uuid"; import { PrismaClient, UserRole } from "./generated/client"; const nodeEnv = process.env.NODE_ENV; -if (!nodeEnv || (nodeEnv !== "development" && nodeEnv !== "production")) { - throw new Error("NODE_ENV is not defined. Please set it to 'development' or 'production'."); +const isDev = nodeEnv === "development"; + +if (!nodeEnv || ["development", "production"].indexOf(nodeEnv) === -1) { + console.log( + "NODE_ENV is not defined.\n\nPlease set it to 'development' or 'production'.\n\nExample: NODE_ENV=development bun run prisma:seed", + ); + + process.exit(1); } const connectionString = `${process.env.DATABASE_URL}`; @@ -21,7 +27,7 @@ const prisma = new PrismaClient({ adapter }); async function main() { await populateMedals(prisma); - if (nodeEnv === "development") { + if (isDev) { await createFirstUser(prisma); } } diff --git a/src/app.module.ts b/src/app.module.ts index 904e7d0..734b60c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,7 @@ import { GameModule } from "./modules/game/game.module"; import { ListModule } from "./modules/list/list.module"; import { MangaModule } from "./modules/manga/manga.module"; import { MovieModule } from "./modules/movie/movie.module"; +import { PaymentModule } from "./modules/payment/payment.module"; import { ProfileModule } from "./modules/profile/profile.module"; import { ReactionModule } from "./modules/reaction/reaction.module"; import { TVShowModule } from "./modules/tv-show/tv-show.module"; @@ -22,21 +23,20 @@ import { HttpThrottlerGuard } from "./shared/guards/http-throttler.guard"; import { CacheModule } from "./shared/infra/cache/cache.module"; import { DatabaseModule } from "./shared/infra/database/database.module"; import { EmailModule } from "./shared/infra/email/email.module"; +import { HealthModule } from "./shared/infra/health/health.module"; import { IntegrationsModule } from "./shared/infra/integrations/integrations.module"; +import { MetricsModule } from "./shared/infra/metrics/metrics.module"; import { QueueModule } from "./shared/infra/queue/queue.module"; import { UploadModule } from "./shared/infra/upload/upload.module"; -import { HealthModule } from "./shared/infra/health/health.module"; import { MetricsInterceptor } from "./shared/interceptors/metrics.interceptor"; -import { MetricsModule } from "./shared/infra/metrics/metrics.module"; -import { PaymentModule } from "./modules/payment/payment.module"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), ThrottlerModule.forRoot({ throttlers: [ - { name: "read", ttl: 60_000, limit: 60, blockDuration: 300_000 }, - { name: "write", ttl: 60_000, limit: 30, blockDuration: 300_000 }, + { name: "read", ttl: 60_000, limit: 200, blockDuration: 300_000 }, + { name: "write", ttl: 60_000, limit: 100, blockDuration: 300_000 }, ], }), MetricsModule, diff --git a/src/modules/anime/anime.module.ts b/src/modules/anime/anime.module.ts index 4b41957..bb67f9c 100644 --- a/src/modules/anime/anime.module.ts +++ b/src/modules/anime/anime.module.ts @@ -1,13 +1,12 @@ import { Module } from "@nestjs/common"; - +import { AnimeController } from "./controller/anime.controller"; import { AnimeEpisodeWatchController } from "./controller/anime-episode-watch.controller"; -import { AnimeEpisodeWatchService } from "./service/anime-episode-watch.service"; import { AnimeProgressController } from "./controller/anime-progress.controller"; -import { AnimeProgressService } from "./service/anime-progress.service"; import { AnimeReviewController } from "./controller/anime-review.controller"; -import { AnimeReviewService } from "./service/anime-review.service"; -import { AnimeController } from "./controller/anime.controller"; import { AnimeService } from "./service/anime.service"; +import { AnimeEpisodeWatchService } from "./service/anime-episode-watch.service"; +import { AnimeProgressService } from "./service/anime-progress.service"; +import { AnimeReviewService } from "./service/anime-review.service"; @Module({ controllers: [AnimeController, AnimeReviewController, AnimeProgressController, AnimeEpisodeWatchController], diff --git a/src/modules/anime/controller/anime-episode-watch.controller.ts b/src/modules/anime/controller/anime-episode-watch.controller.ts index 88b3597..9ad1a4e 100644 --- a/src/modules/anime/controller/anime-episode-watch.controller.ts +++ b/src/modules/anime/controller/anime-episode-watch.controller.ts @@ -1,12 +1,12 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { CreateOrUpdateAnimeEpisodeWatchDto } from "../dto/create-or-update-anime-episode-watch.dto"; -import { DeleteAnimeEpisodeWatchDto } from "../dto/delete-anime-episode-watch.dto"; import { DeleteAllAnimeEpisodeWatchDto } from "../dto/delete-all-anime-episode-watch.dto"; +import { DeleteAnimeEpisodeWatchDto } from "../dto/delete-anime-episode-watch.dto"; +import { GetAnimeEpisodeWatchDto } from "../dto/get-anime-episode-watch.dto"; import { WatchAllAnimeEpisodesDto } from "../dto/watch-all-anime-episodes.dto"; import { AnimeEpisodeWatchService } from "../service/anime-episode-watch.service"; -import { GetAnimeEpisodeWatchDto } from "../dto/get-anime-episode-watch.dto"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("Anime") @Controller("/anime/episode/watch") diff --git a/src/modules/anime/controller/anime-progress.controller.ts b/src/modules/anime/controller/anime-progress.controller.ts index 882b61c..ca78da4 100644 --- a/src/modules/anime/controller/anime-progress.controller.ts +++ b/src/modules/anime/controller/anime-progress.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { AnimeProgressService } from "../service/anime-progress.service"; import { CreateOrUpdateAnimeProgressDto } from "../dto/create-or-update-anime-progress.dto"; import { GetAnimeProgressDto } from "../dto/get-anime-progress.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { AnimeProgressService } from "../service/anime-progress.service"; @ApiTags("Anime") @Controller("/anime/progress") diff --git a/src/modules/anime/controller/anime-review.controller.ts b/src/modules/anime/controller/anime-review.controller.ts index ab5defa..0ac3461 100644 --- a/src/modules/anime/controller/anime-review.controller.ts +++ b/src/modules/anime/controller/anime-review.controller.ts @@ -12,12 +12,12 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { AnimeReviewService } from "../service/anime-review.service"; import { CreateAnimeReviewDto } from "../dto/create-anime-review.dto"; import { GetAnimeReviewsDto } from "../dto/get-anime-reviews.dto"; import { UpdateAnimeReviewDto } from "../dto/update-anime-review.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { AnimeReviewService } from "../service/anime-review.service"; @ApiTags("Anime") @Controller("/anime/review") diff --git a/src/modules/anime/controller/anime.controller.ts b/src/modules/anime/controller/anime.controller.ts index b88e56b..8436e18 100644 --- a/src/modules/anime/controller/anime.controller.ts +++ b/src/modules/anime/controller/anime.controller.ts @@ -1,11 +1,11 @@ import { Body, Controller, Get, Param, ParseIntPipe, Post, Query, UseGuards } from "@nestjs/common"; import { ApiTags } from "@nestjs/swagger"; import { AuthGuard } from "@thallesp/nestjs-better-auth"; +import { GetAnimeEpisodesByMalIdDto } from "../dto/get-anime-episodes-by-mal-id.dto"; import { RefreshAnimeDto } from "../dto/refresh-anime.dto"; import { SearchAnimeDto } from "../dto/search-anime.dto"; import { TopAnimeDto } from "../dto/top-anime.dto"; import { AnimeService } from "../service/anime.service"; -import { GetAnimeEpisodesByMalIdDto } from "../dto/get-anime-episodes-by-mal-id.dto"; @ApiTags("Anime") @Controller("/anime") diff --git a/src/modules/anime/dto/get-anime-reviews.dto.ts b/src/modules/anime/dto/get-anime-reviews.dto.ts index 1773191..c79a38a 100644 --- a/src/modules/anime/dto/get-anime-reviews.dto.ts +++ b/src/modules/anime/dto/get-anime-reviews.dto.ts @@ -1,5 +1,5 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional, IsUUID } from "class-validator"; +import { IsOptional, IsString, IsUUID } from "class-validator"; import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetAnimeReviewsDto extends OffsetPaginationParamsDto { @@ -12,4 +12,13 @@ export class GetAnimeReviewsDto extends OffsetPaginationParamsDto { @IsUUID() @ApiPropertyOptional({ type: "string", format: "uuid" }) readonly userId?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: "Search query to filter reviews by media title", + example: "naruto", + type: "string", + }) + readonly query?: string; } diff --git a/src/modules/anime/dto/search-anime.dto.ts b/src/modules/anime/dto/search-anime.dto.ts index 3a9f33f..c916c39 100644 --- a/src/modules/anime/dto/search-anime.dto.ts +++ b/src/modules/anime/dto/search-anime.dto.ts @@ -1,13 +1,13 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Transform, Type } from "class-transformer"; +import { IsArray, IsEnum, IsInt, IsOptional, IsPositive, Matches } from "class-validator"; import { - JikanAnimeType, - JikanAnimeStatus, + JikanAnimeOrderBy, JikanAnimeRatings, + JikanAnimeStatus, + JikanAnimeType, JikanSort, - JikanAnimeOrderBy, } from "@/shared/infra/integrations/jikan.service"; -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { Transform, Type } from "class-transformer"; -import { IsArray, IsEnum, IsInt, IsOptional, IsPositive, Matches } from "class-validator"; export class SearchAnimeDto { @IsOptional() diff --git a/src/modules/anime/service/anime-episode-watch.service.ts b/src/modules/anime/service/anime-episode-watch.service.ts index d1ddacb..41f7f9a 100644 --- a/src/modules/anime/service/anime-episode-watch.service.ts +++ b/src/modules/anime/service/anime-episode-watch.service.ts @@ -1,13 +1,13 @@ import { Injectable } from "@nestjs/common"; -import { CreateOrUpdateAnimeEpisodeWatchDto } from "../dto/create-or-update-anime-episode-watch.dto"; -import { DatabaseService } from "@/shared/infra/database/database.service"; -import { GetAnimeEpisodeWatchDto } from "../dto/get-anime-episode-watch.dto"; -import { AppException } from "@/shared/exceptions/app.exceptions"; +import { WatchEpisodeStatus } from "@prisma/generated/enums"; import { ERROR_CODES } from "@/shared/constants/error-codes"; -import { DeleteAnimeEpisodeWatchDto } from "../dto/delete-anime-episode-watch.dto"; +import { AppException } from "@/shared/exceptions/app.exceptions"; +import { DatabaseService } from "@/shared/infra/database/database.service"; +import { CreateOrUpdateAnimeEpisodeWatchDto } from "../dto/create-or-update-anime-episode-watch.dto"; import { DeleteAllAnimeEpisodeWatchDto } from "../dto/delete-all-anime-episode-watch.dto"; +import { DeleteAnimeEpisodeWatchDto } from "../dto/delete-anime-episode-watch.dto"; +import { GetAnimeEpisodeWatchDto } from "../dto/get-anime-episode-watch.dto"; import { WatchAllAnimeEpisodesDto } from "../dto/watch-all-anime-episodes.dto"; -import { WatchEpisodeStatus } from "@prisma/generated/enums"; @Injectable() export class AnimeEpisodeWatchService { diff --git a/src/modules/anime/service/anime-progress.service.ts b/src/modules/anime/service/anime-progress.service.ts index 40b8a1e..2a1b7aa 100644 --- a/src/modules/anime/service/anime-progress.service.ts +++ b/src/modules/anime/service/anime-progress.service.ts @@ -1,13 +1,13 @@ -import { DatabaseService } from "@/shared/infra/database/database.service"; import { Injectable } from "@nestjs/common"; -import { CreateOrUpdateAnimeProgressDto } from "../dto/create-or-update-anime-progress.dto"; -import { GetAnimeProgressDto } from "../dto/get-anime-progress.dto"; +import { FeedEventType, ProgressStatus } from "@prisma/generated/enums"; import { AnimeProgressFindManyArgs } from "@prisma/generated/models"; -import { AppException } from "@/shared/exceptions/app.exceptions"; import { ERROR_CODES } from "@/shared/constants/error-codes"; -import { AnimeEpisodeWatchService } from "./anime-episode-watch.service"; -import { FeedEventType, ProgressStatus } from "@prisma/generated/enums"; +import { AppException } from "@/shared/exceptions/app.exceptions"; +import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; +import { CreateOrUpdateAnimeProgressDto } from "../dto/create-or-update-anime-progress.dto"; +import { GetAnimeProgressDto } from "../dto/get-anime-progress.dto"; +import { AnimeEpisodeWatchService } from "./anime-episode-watch.service"; @Injectable() export class AnimeProgressService { diff --git a/src/modules/anime/service/anime-review.service.ts b/src/modules/anime/service/anime-review.service.ts index 00ed898..4a6a9e7 100644 --- a/src/modules/anime/service/anime-review.service.ts +++ b/src/modules/anime/service/anime-review.service.ts @@ -109,7 +109,11 @@ export class AnimeReviewService { where: { ...(getAnimeReviewsDto.animeId && { animeId: getAnimeReviewsDto.animeId }), ...(getAnimeReviewsDto.userId && { userId: getAnimeReviewsDto.userId }), + ...(getAnimeReviewsDto.query && { + anime: { title: { contains: getAnimeReviewsDto.query, mode: "insensitive" } }, + }), }, + orderBy: { createdAt: "desc" }, include: { anime: { select: { @@ -132,6 +136,19 @@ export class AnimeReviewService { }, }, }, + reactions: { + select: { + id: true, + emoji: true, + createdAt: true, + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, }, }); diff --git a/src/modules/anime/service/anime.service.ts b/src/modules/anime/service/anime.service.ts index 0c2b0ab..126f26b 100644 --- a/src/modules/anime/service/anime.service.ts +++ b/src/modules/anime/service/anime.service.ts @@ -1,11 +1,9 @@ import { Injectable } from "@nestjs/common"; -import { Anime, ProgressStatus } from "@prisma/generated/client"; +import { ProgressStatus } from "@prisma/generated/client"; import { AnimeCreateInput, AnimeUpdateInput } from "@prisma/generated/models"; -import { CACHE_KEYS } from "@/shared/constants/cache"; import { ERROR_CODES } from "@/shared/constants/error-codes"; import { REFRESH_INTERVAL_MS } from "@/shared/constants/refresh-interval"; import { AppException } from "@/shared/exceptions/app.exceptions"; -import { CacheService } from "@/shared/infra/cache/cache.service"; import { DatabaseService, DEFAULT_PAGINATION_PAGE } from "@/shared/infra/database/database.service"; import { IntegrationsService } from "@/shared/infra/integrations/integrations.service"; import { @@ -15,15 +13,14 @@ import { JikanAnimeType, JikanSort, } from "@/shared/infra/integrations/jikan.service"; +import { GetAnimeEpisodesByMalIdDto } from "../dto/get-anime-episodes-by-mal-id.dto"; import type { RefreshAnimeDto } from "../dto/refresh-anime.dto"; import type { SearchAnimeDto } from "../dto/search-anime.dto"; import { TopAnimeDto } from "../dto/top-anime.dto"; -import { GetAnimeEpisodesByMalIdDto } from "../dto/get-anime-episodes-by-mal-id.dto"; @Injectable() export class AnimeService { constructor( - private readonly cacheService: CacheService, private readonly databaseService: DatabaseService, private readonly integrationsService: IntegrationsService, ) {} @@ -105,14 +102,6 @@ export class AnimeService { } async getAnimeByMalId(malId: number) { - const animeDetailKey = CACHE_KEYS.ANIME_BY_MAL_ID.prefix(malId); - - const cachedAnime = await this.cacheService.get(animeDetailKey); - - if (cachedAnime) { - return cachedAnime; - } - let anime = await this.databaseService.anime.findUnique({ where: { malId }, omit: { @@ -163,19 +152,10 @@ export class AnimeService { progressStats, }; - await this.cacheService.set(animeDetailKey, animeWithStats, CACHE_KEYS.ANIME_BY_MAL_ID.expiration); - return animeWithStats; } async getAnimeRelationsByMalId(malId: number) { - const cachedRelationsKey = CACHE_KEYS.ANIME_RELATIONS_BY_MAL_ID.prefix(malId); - const cachedRelations = await this.cacheService.get(cachedRelationsKey); - - if (cachedRelations) { - return cachedRelations; - } - const anime = await this.databaseService.anime.findUnique({ where: { malId }, select: { relations: true }, @@ -186,15 +166,11 @@ export class AnimeService { } if (anime.relations) { - await this.cacheService.set(cachedRelationsKey, anime.relations, CACHE_KEYS.ANIME_RELATIONS_BY_MAL_ID.expiration); - return anime.relations; } const relations = await this.integrationsService.jikan.getAnimeRelationsById(malId); - await this.cacheService.set(cachedRelationsKey, relations, CACHE_KEYS.ANIME_RELATIONS_BY_MAL_ID.expiration); - await this.databaseService.anime.update({ where: { malId }, data: { relations }, @@ -207,13 +183,6 @@ export class AnimeService { const { malId, page = DEFAULT_PAGINATION_PAGE } = getAnimeEpisodesByMalIdDto; const pageKey = String(page); - const cachedEpisodesKey = CACHE_KEYS.ANIME_EPISODES_BY_MAL_ID.prefix(getAnimeEpisodesByMalIdDto); - const cachedEpisodes = await this.cacheService.get(cachedEpisodesKey); - - if (cachedEpisodes) { - return cachedEpisodes; - } - const anime = await this.databaseService.anime.findUnique({ where: { malId }, select: { episodes: true }, @@ -226,12 +195,6 @@ export class AnimeService { const storedEpisodes = (anime.episodes ?? {}) as Record; if (storedEpisodes[pageKey]) { - await this.cacheService.set( - cachedEpisodesKey, - storedEpisodes[pageKey], - CACHE_KEYS.ANIME_EPISODES_BY_MAL_ID.expiration, - ); - return storedEpisodes[pageKey]; } @@ -242,8 +205,6 @@ export class AnimeService { data: { episodes: { ...storedEpisodes, [pageKey]: episodes } as any }, }); - await this.cacheService.set(cachedEpisodesKey, episodes, CACHE_KEYS.ANIME_EPISODES_BY_MAL_ID.expiration); - return episodes; } @@ -264,12 +225,6 @@ export class AnimeService { throw new AppException(ERROR_CODES.ANIME_ALREADY_REFRESHED); } - const animeDetailKey = CACHE_KEYS.ANIME_BY_MAL_ID.prefix(refreshAnimeDto.malId); - - if (await this.cacheService.exists(animeDetailKey)) { - await this.cacheService.delete(animeDetailKey); - } - const existingEpisodes = anime.episodes ?? {}; const episodesPageKeys = Object.keys(existingEpisodes); @@ -281,16 +236,8 @@ export class AnimeService { page: Number(pageKey), }; - const cachedEpisodesKey = CACHE_KEYS.ANIME_EPISODES_BY_MAL_ID.prefix(getAnimeEpisodesByMalIdDto); - - if (await this.cacheService.exists(cachedEpisodesKey)) { - await this.cacheService.delete(cachedEpisodesKey); - } - const episodes = await this.integrationsService.jikan.getAnimeEpisodesById(getAnimeEpisodesByMalIdDto); - await this.cacheService.set(cachedEpisodesKey, episodes, CACHE_KEYS.ANIME_EPISODES_BY_MAL_ID.expiration); - jikanEpisodes[pageKey] = episodes; await new Promise((resolve) => setTimeout(resolve, 800)); diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index d7bf7f7..2351a75 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -4,9 +4,9 @@ import { AuthModule as BetterAuthModule } from "@thallesp/nestjs-better-auth"; import { betterAuth } from "better-auth"; import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; +import { ProfileService } from "../profile/service/profile.service"; import { UserService } from "../user/service/user.service"; import { getAuthConfig } from "./config/auth.config"; -import { ProfileService } from "../profile/service/profile.service"; @Global() @Module({ diff --git a/src/modules/auth/config/auth.config.ts b/src/modules/auth/config/auth.config.ts index 7f3a7d5..18c94d6 100644 --- a/src/modules/auth/config/auth.config.ts +++ b/src/modules/auth/config/auth.config.ts @@ -1,14 +1,14 @@ import type { BetterAuthOptions } from "@better-auth/core"; +import { Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; +import * as bcrypt from "bcrypt"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { bearer, customSession, lastLoginMethod, magicLink, openAPI, username } from "better-auth/plugins"; +import uuid from "uuid"; import type { ProfileService } from "@/modules/profile/service/profile.service"; import type { UserService } from "@/modules/user/service/user.service"; import type { DatabaseService } from "@/shared/infra/database/database.service"; import type { QueueService } from "@/shared/infra/queue/queue.service"; -import { Logger } from "@nestjs/common"; -import uuid from "uuid"; -import * as bcrypt from "bcrypt"; interface AuthConfigParams { configService?: ConfigService; diff --git a/src/modules/book/book.module.ts b/src/modules/book/book.module.ts index 2f75dd0..16e3d82 100644 --- a/src/modules/book/book.module.ts +++ b/src/modules/book/book.module.ts @@ -1,11 +1,10 @@ import { Module } from "@nestjs/common"; - +import { BookController } from "./controller/book.controller"; import { BookProgressController } from "./controller/book-progress.controller"; -import { BookProgressService } from "./service/book-progress.service"; import { BookReviewController } from "./controller/book-review.controller"; -import { BookReviewService } from "./service/book-review.service"; -import { BookController } from "./controller/book.controller"; import { BookService } from "./service/book.service"; +import { BookProgressService } from "./service/book-progress.service"; +import { BookReviewService } from "./service/book-review.service"; @Module({ imports: [], diff --git a/src/modules/book/controller/book-progress.controller.ts b/src/modules/book/controller/book-progress.controller.ts index 7d7590e..25cb27f 100644 --- a/src/modules/book/controller/book-progress.controller.ts +++ b/src/modules/book/controller/book-progress.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { BookProgressService } from "../service/book-progress.service"; import { CreateOrUpdateBookProgressDto } from "../dto/create-or-update-book-progress.dto"; import { GetBookProgressDto } from "../dto/get-book-progress.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { BookProgressService } from "../service/book-progress.service"; @ApiTags("Book") @Controller("/book/progress") diff --git a/src/modules/book/controller/book-review.controller.ts b/src/modules/book/controller/book-review.controller.ts index 09d9c96..147fc70 100644 --- a/src/modules/book/controller/book-review.controller.ts +++ b/src/modules/book/controller/book-review.controller.ts @@ -12,12 +12,12 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { BookReviewService } from "../service/book-review.service"; import { CreateBookReviewDto } from "../dto/create-book-review.dto"; import { GetBookReviewsDto } from "../dto/get-book-reviews.dto"; import { UpdateBookReviewDto } from "../dto/update-book-review.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { BookReviewService } from "../service/book-review.service"; @ApiTags("Book") @Controller("/book/review") diff --git a/src/modules/book/dto/get-book-reviews.dto.ts b/src/modules/book/dto/get-book-reviews.dto.ts index a19931a..7264e67 100644 --- a/src/modules/book/dto/get-book-reviews.dto.ts +++ b/src/modules/book/dto/get-book-reviews.dto.ts @@ -1,5 +1,5 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional, IsUUID } from "class-validator"; +import { IsOptional, IsString, IsUUID } from "class-validator"; import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetBookReviewsDto extends OffsetPaginationParamsDto { @@ -20,4 +20,13 @@ export class GetBookReviewsDto extends OffsetPaginationParamsDto { type: "string", }) readonly userId?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: "Search query to filter reviews by media title", + example: "dune", + type: "string", + }) + readonly query?: string; } diff --git a/src/modules/book/dto/search-book.dto.ts b/src/modules/book/dto/search-book.dto.ts index 782785c..c90ed98 100644 --- a/src/modules/book/dto/search-book.dto.ts +++ b/src/modules/book/dto/search-book.dto.ts @@ -1,7 +1,7 @@ -import { HardcoverBookOrderBy } from "@/shared/infra/integrations/hardcover.service"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Transform, Type } from "class-transformer"; import { IsArray, IsEnum, IsInt, IsOptional, IsPositive, IsString, Matches } from "class-validator"; +import { HardcoverBookOrderBy } from "@/shared/infra/integrations/hardcover.service"; export class SearchBookDto { @IsOptional() diff --git a/src/modules/book/service/book-progress.service.ts b/src/modules/book/service/book-progress.service.ts index 5204a44..9b56fad 100644 --- a/src/modules/book/service/book-progress.service.ts +++ b/src/modules/book/service/book-progress.service.ts @@ -1,12 +1,12 @@ -import { DatabaseService } from "@/shared/infra/database/database.service"; import { Injectable } from "@nestjs/common"; -import { CreateOrUpdateBookProgressDto } from "../dto/create-or-update-book-progress.dto"; -import { GetBookProgressDto } from "../dto/get-book-progress.dto"; +import { FeedEventType } from "@prisma/generated/enums"; import { BookProgressFindManyArgs } from "@prisma/generated/models"; -import { AppException } from "@/shared/exceptions/app.exceptions"; import { ERROR_CODES } from "@/shared/constants/error-codes"; +import { AppException } from "@/shared/exceptions/app.exceptions"; +import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; -import { FeedEventType } from "@prisma/generated/enums"; +import { CreateOrUpdateBookProgressDto } from "../dto/create-or-update-book-progress.dto"; +import { GetBookProgressDto } from "../dto/get-book-progress.dto"; @Injectable() export class BookProgressService { diff --git a/src/modules/book/service/book-review.service.ts b/src/modules/book/service/book-review.service.ts index f754a45..b4a2a75 100644 --- a/src/modules/book/service/book-review.service.ts +++ b/src/modules/book/service/book-review.service.ts @@ -105,7 +105,11 @@ export class BookReviewService { where: { ...(getBookReviewsDto.bookId && { bookId: getBookReviewsDto.bookId }), ...(getBookReviewsDto.userId && { userId: getBookReviewsDto.userId }), + ...(getBookReviewsDto.query && { + book: { title: { contains: getBookReviewsDto.query, mode: "insensitive" } }, + }), }, + orderBy: { createdAt: "desc" }, include: { book: { select: { @@ -128,6 +132,19 @@ export class BookReviewService { }, }, }, + reactions: { + select: { + id: true, + emoji: true, + createdAt: true, + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, }, }); diff --git a/src/modules/book/service/book.service.ts b/src/modules/book/service/book.service.ts index 36bfd4d..3aed9e1 100644 --- a/src/modules/book/service/book.service.ts +++ b/src/modules/book/service/book.service.ts @@ -1,22 +1,19 @@ import { Injectable } from "@nestjs/common"; -import { Book, ProgressStatus } from "@prisma/generated/client"; +import { ProgressStatus } from "@prisma/generated/client"; import { BookCreateInput, BookUpdateInput } from "@prisma/generated/models"; import { TopBookDto } from "@/modules/book/dto/top-book.dto"; -import { CACHE_KEYS } from "@/shared/constants/cache"; import { ERROR_CODES } from "@/shared/constants/error-codes"; import { REFRESH_INTERVAL_MS } from "@/shared/constants/refresh-interval"; import { AppException } from "@/shared/exceptions/app.exceptions"; -import { CacheService } from "@/shared/infra/cache/cache.service"; import { DatabaseService } from "@/shared/infra/database/database.service"; +import { HardcoverBookOrderBy, HardcoverSort } from "@/shared/infra/integrations/hardcover.service"; import { IntegrationsService } from "@/shared/infra/integrations/integrations.service"; import type { RefreshBookDto } from "../dto/refresh-book.dto"; import type { SearchBookDto } from "../dto/search-book.dto"; -import { HardcoverBookOrderBy, HardcoverSort } from "@/shared/infra/integrations/hardcover.service"; @Injectable() export class BookService { constructor( - private readonly cacheService: CacheService, private readonly databaseService: DatabaseService, private readonly integrationsService: IntegrationsService, ) {} @@ -94,12 +91,6 @@ export class BookService { } async getBookByHardcoverId(hardcoverId: number) { - const cachedBook = await this.cacheService.get(CACHE_KEYS.BOOK_BY_HARDCOVER_ID.prefix(hardcoverId)); - - if (cachedBook) { - return cachedBook; - } - let book = await this.databaseService.book.findUnique({ where: { hardcoverId }, }); @@ -146,12 +137,6 @@ export class BookService { progressStats, }; - await this.cacheService.set( - CACHE_KEYS.BOOK_BY_HARDCOVER_ID.prefix(hardcoverId), - bookWithStats, - CACHE_KEYS.BOOK_BY_HARDCOVER_ID.expiration, - ); - return bookWithStats; } @@ -171,10 +156,6 @@ export class BookService { throw new AppException(ERROR_CODES.BOOK_ALREADY_REFRESHED); } - if (await this.cacheService.exists(CACHE_KEYS.BOOK_BY_HARDCOVER_ID.prefix(refreshBookDto.hardcoverId))) { - await this.cacheService.delete(CACHE_KEYS.BOOK_BY_HARDCOVER_ID.prefix(refreshBookDto.hardcoverId)); - } - const hardcoverBook = await this.integrationsService.hardcover.getBookByHardcoverId(refreshBookDto.hardcoverId); await this.databaseService.book.update({ diff --git a/src/modules/comment/controller/comment.controller.ts b/src/modules/comment/controller/comment.controller.ts index 0e4afe1..f48f379 100644 --- a/src/modules/comment/controller/comment.controller.ts +++ b/src/modules/comment/controller/comment.controller.ts @@ -11,11 +11,11 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { CommentService } from "../service/comment.service"; import { CreateCommentDto } from "../dto/create-comment.dto"; import { GetCommentsDto } from "../dto/get-comments.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { CommentService } from "../service/comment.service"; @ApiTags("Comment") @Controller("/comment") diff --git a/src/modules/comment/dto/create-comment.dto.ts b/src/modules/comment/dto/create-comment.dto.ts index a741423..257d172 100644 --- a/src/modules/comment/dto/create-comment.dto.ts +++ b/src/modules/comment/dto/create-comment.dto.ts @@ -1,7 +1,14 @@ import { ApiProperty } from "@nestjs/swagger"; import { CommentType } from "@prisma/generated/enums"; -import { IsEnum, IsNotEmpty, IsOptional, MaxLength } from "class-validator"; -import { registerDecorator, ValidationArguments, ValidationOptions } from "class-validator"; +import { + IsEnum, + IsNotEmpty, + IsOptional, + MaxLength, + registerDecorator, + ValidationArguments, + ValidationOptions, +} from "class-validator"; export function CommentRequiredForType(commentType: CommentType, options?: ValidationOptions) { return (object: object, propertyName: string) => { diff --git a/src/modules/comment/service/comment.service.ts b/src/modules/comment/service/comment.service.ts index 87f6253..961b779 100644 --- a/src/modules/comment/service/comment.service.ts +++ b/src/modules/comment/service/comment.service.ts @@ -27,12 +27,20 @@ export class CommentService { async deleteComment(deleteCommentDto: DeleteCommentDto) { const comment = await this.databaseService.comment.findUnique({ where: { id: deleteCommentDto.commentId }, + include: { profile: { select: { userId: true } } }, }); - if (!comment || comment.userId !== deleteCommentDto.userId) { + if (!comment) { throw new AppException(ERROR_CODES.COMMENT_NOT_FOUND); } + const isAuthor = comment.userId === deleteCommentDto.userId; + const isProfileOwner = comment.profile?.userId === deleteCommentDto.userId; + + if (!isAuthor && !isProfileOwner) { + throw new AppException(ERROR_CODES.UNAUTHORIZED); + } + await this.databaseService.comment.delete({ where: { id: comment.id }, }); @@ -44,6 +52,7 @@ export class CommentService { where: { ...getCommentsDto }, page, itemsPerPage, + orderBy: { createdAt: "desc" }, include: { user: { select: { diff --git a/src/modules/favorite/controller/favorite.controller.ts b/src/modules/favorite/controller/favorite.controller.ts index ef10aa5..669353d 100644 --- a/src/modules/favorite/controller/favorite.controller.ts +++ b/src/modules/favorite/controller/favorite.controller.ts @@ -11,12 +11,13 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { AddFavoriteDto } from "../dto/add-favorite.dto"; +import { GetFavoriteStatusDto } from "../dto/get-favorite-status.dto"; import { GetFavoritesByUserIdDto } from "../dto/get-favorites-by-user-id.dto"; import { RemoveFavoriteDto } from "../dto/remove-favorite.dto"; import { FavoriteService } from "../service/favorite.service"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("Favorite") @Controller("/favorite") @@ -42,6 +43,17 @@ export class FavoriteController { }); } + @Get("/status") + @UseGuards(AuthGuard) + async getFavoriteStatus(@Session() session: UserSession, @Query() query: GetFavoriteStatusDto) { + const favorited = await this.favoriteService.getFavoriteStatus({ + ...query, + userId: session.user.id, + }); + + return { favorited }; + } + @Get("/user/:userId") async getFavoritesByUserId( @Param("userId", new ParseUUIDPipe()) userId: string, diff --git a/src/modules/favorite/dto/add-favorite.dto.ts b/src/modules/favorite/dto/add-favorite.dto.ts index ec33789..d5e05dd 100644 --- a/src/modules/favorite/dto/add-favorite.dto.ts +++ b/src/modules/favorite/dto/add-favorite.dto.ts @@ -52,37 +52,37 @@ export class AddFavoriteDto { @IsOptional() @FavoriteRequiredForType(FavoriteType.Anime) - @IsUUID("4") + @IsUUID() @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Anime" }) readonly animeId?: string; @IsOptional() @FavoriteRequiredForType(FavoriteType.Manga) - @IsUUID("4") + @IsUUID() @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Manga" }) readonly mangaId?: string; @IsOptional() @FavoriteRequiredForType(FavoriteType.TVShow) - @IsUUID("4") + @IsUUID() @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is TVShow" }) readonly tvShowId?: string; @IsOptional() @FavoriteRequiredForType(FavoriteType.Movie) - @IsUUID("4") + @IsUUID() @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Movie" }) readonly movieId?: string; @IsOptional() @FavoriteRequiredForType(FavoriteType.Game) - @IsUUID("4") + @IsUUID() @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Game" }) readonly gameId?: string; @IsOptional() @FavoriteRequiredForType(FavoriteType.Book) - @IsUUID("4") + @IsUUID() @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Book" }) readonly bookId?: string; } diff --git a/src/modules/favorite/dto/get-favorite-status.dto.ts b/src/modules/favorite/dto/get-favorite-status.dto.ts new file mode 100644 index 0000000..3f66650 --- /dev/null +++ b/src/modules/favorite/dto/get-favorite-status.dto.ts @@ -0,0 +1,49 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { FavoriteType } from "@prisma/generated/enums"; +import { IsEnum, IsOptional, IsUUID } from "class-validator"; +import { FavoriteRequiredForType } from "./add-favorite.dto"; + +export class GetFavoriteStatusDto { + @IsEnum(FavoriteType) + @ApiProperty({ enum: FavoriteType }) + readonly type: FavoriteType; + + @ApiProperty({ type: "string", format: "uuid" }) + readonly userId: string; + + @IsOptional() + @FavoriteRequiredForType(FavoriteType.Anime) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Anime" }) + readonly animeId?: string; + + @IsOptional() + @FavoriteRequiredForType(FavoriteType.Manga) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Manga" }) + readonly mangaId?: string; + + @IsOptional() + @FavoriteRequiredForType(FavoriteType.TVShow) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is TVShow" }) + readonly tvShowId?: string; + + @IsOptional() + @FavoriteRequiredForType(FavoriteType.Movie) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Movie" }) + readonly movieId?: string; + + @IsOptional() + @FavoriteRequiredForType(FavoriteType.Game) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Game" }) + readonly gameId?: string; + + @IsOptional() + @FavoriteRequiredForType(FavoriteType.Book) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Book" }) + readonly bookId?: string; +} diff --git a/src/modules/favorite/dto/get-favorites-by-user-id.dto.ts b/src/modules/favorite/dto/get-favorites-by-user-id.dto.ts index 7cf2ee9..7ea5abb 100644 --- a/src/modules/favorite/dto/get-favorites-by-user-id.dto.ts +++ b/src/modules/favorite/dto/get-favorites-by-user-id.dto.ts @@ -1,7 +1,17 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsOptional, IsString } from "class-validator"; import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetFavoritesByUserIdDto extends OffsetPaginationParamsDto { @ApiProperty({ type: "string", format: "uuid" }) readonly userId: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: "Search query to filter favorites by entity name", + example: "naruto", + type: "string", + }) + readonly query?: string; } diff --git a/src/modules/favorite/service/favorite.service.ts b/src/modules/favorite/service/favorite.service.ts index 5b19a5e..e72b5cb 100644 --- a/src/modules/favorite/service/favorite.service.ts +++ b/src/modules/favorite/service/favorite.service.ts @@ -6,6 +6,7 @@ import { AppException } from "@/shared/exceptions/app.exceptions"; import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; import { AddFavoriteDto } from "../dto/add-favorite.dto"; +import { GetFavoriteStatusDto } from "../dto/get-favorite-status.dto"; import { GetFavoritesByUserIdDto } from "../dto/get-favorites-by-user-id.dto"; import { RemoveFavoriteDto } from "../dto/remove-favorite.dto"; @@ -19,6 +20,18 @@ export class FavoriteService { async addFavorite(addFavoriteDto: AddFavoriteDto) { const { type, userId, position, ...entityIds } = addFavoriteDto; + const favoriteAlreadyExists = await this.databaseService.favorite.findFirst({ + where: { + type, + userId, + ...entityIds, + }, + }); + + if (favoriteAlreadyExists) { + throw new AppException(ERROR_CODES.FAVORITE_ALREADY_EXISTS); + } + const favorite = await this.databaseService.favorite.create({ data: { type, @@ -111,7 +124,19 @@ export class FavoriteService { model: "favorite", itemsPerPage: getFavoritesByUserIdDto.itemsPerPage, page: getFavoritesByUserIdDto.page, - where: { userId: getFavoritesByUserIdDto.userId }, + where: { + userId: getFavoritesByUserIdDto.userId, + ...(getFavoritesByUserIdDto.query && { + OR: [ + { anime: { title: { contains: getFavoritesByUserIdDto.query, mode: "insensitive" as const } } }, + { manga: { title: { contains: getFavoritesByUserIdDto.query, mode: "insensitive" as const } } }, + { tvShow: { name: { contains: getFavoritesByUserIdDto.query, mode: "insensitive" as const } } }, + { movie: { title: { contains: getFavoritesByUserIdDto.query, mode: "insensitive" as const } } }, + { game: { name: { contains: getFavoritesByUserIdDto.query, mode: "insensitive" as const } } }, + { book: { title: { contains: getFavoritesByUserIdDto.query, mode: "insensitive" as const } } }, + ], + }), + }, select: { id: true, type: true, @@ -183,6 +208,21 @@ export class FavoriteService { return favorites; } + async getFavoriteStatus(getFavoriteStatusDto: GetFavoriteStatusDto & { userId: string }) { + const { type, userId, ...entityIds } = getFavoriteStatusDto; + + const favorite = await this.databaseService.favorite.findFirst({ + where: { + type, + userId, + ...entityIds, + }, + select: { id: true }, + }); + + return !!favorite; + } + async removeFavorite(removeFavoriteDto: RemoveFavoriteDto) { const { type, userId, position: _position, ...entityIds } = removeFavoriteDto; diff --git a/src/modules/feed-event/controller/feed-event.controller.ts b/src/modules/feed-event/controller/feed-event.controller.ts index f69fad8..b61db7a 100644 --- a/src/modules/feed-event/controller/feed-event.controller.ts +++ b/src/modules/feed-event/controller/feed-event.controller.ts @@ -1,9 +1,9 @@ import { Controller, Get, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { GetFeedEventsDto } from "../dto/get-feed-events.dto"; import { GetFeedEventsByUserDto } from "../dto/get-feed-events-by-user.dto"; import { FeedEventService } from "../service/feed-event.service"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("Feed Event") @Controller("/feed") diff --git a/src/modules/game/controller/game-progress.controller.ts b/src/modules/game/controller/game-progress.controller.ts index 5a5d318..e463abb 100644 --- a/src/modules/game/controller/game-progress.controller.ts +++ b/src/modules/game/controller/game-progress.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { GameProgressService } from "../service/game-progress.service"; import { CreateOrUpdateGameProgressDto } from "../dto/create-or-update-game-progress.dto"; import { GetGameProgressDto } from "../dto/get-game-progress.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { GameProgressService } from "../service/game-progress.service"; @ApiTags("Game") @Controller("/game/progress") diff --git a/src/modules/game/controller/game-review-screenshot.controller.ts b/src/modules/game/controller/game-review-screenshot.controller.ts index cfa7337..13752c4 100644 --- a/src/modules/game/controller/game-review-screenshot.controller.ts +++ b/src/modules/game/controller/game-review-screenshot.controller.ts @@ -11,8 +11,8 @@ import { Query, UseGuards, } from "@nestjs/common"; -import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { ApiTags } from "@nestjs/swagger"; +import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { CreateGameReviewScreenshotDto } from "../dto/create-game-review-screenshot.dto"; import { GetGameReviewScreenshotsDto } from "../dto/get-game-review-screenshots.dto"; import { GameReviewScreenshotService } from "../service/game-review-screenshot.service"; diff --git a/src/modules/game/controller/game-review.controller.ts b/src/modules/game/controller/game-review.controller.ts index 1dc38f6..621a80a 100644 --- a/src/modules/game/controller/game-review.controller.ts +++ b/src/modules/game/controller/game-review.controller.ts @@ -12,11 +12,11 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { CreateGameReviewDto } from "../dto/create-game-review.dto"; import { GetGameReviewsDto } from "../dto/get-game-reviews.dto"; import { GameReviewService } from "../service/game-review.service"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("Game") @Controller("/game/review") diff --git a/src/modules/game/dto/get-game-reviews.dto.ts b/src/modules/game/dto/get-game-reviews.dto.ts index e673d84..338ee38 100644 --- a/src/modules/game/dto/get-game-reviews.dto.ts +++ b/src/modules/game/dto/get-game-reviews.dto.ts @@ -1,5 +1,5 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional, IsUUID } from "class-validator"; +import { IsOptional, IsString, IsUUID } from "class-validator"; import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetGameReviewsDto extends OffsetPaginationParamsDto { @@ -20,4 +20,13 @@ export class GetGameReviewsDto extends OffsetPaginationParamsDto { type: "string", }) readonly userId?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: "Search query to filter reviews by media title", + example: "zelda", + type: "string", + }) + readonly query?: string; } diff --git a/src/modules/game/dto/search-game.dto.ts b/src/modules/game/dto/search-game.dto.ts index 914475f..f2f141e 100644 --- a/src/modules/game/dto/search-game.dto.ts +++ b/src/modules/game/dto/search-game.dto.ts @@ -1,7 +1,7 @@ -import { IGDBGameOrderBy, IGDBSort } from "@/shared/infra/integrations/igdb.service"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Transform, Type } from "class-transformer"; import { IsArray, IsEnum, IsInt, IsOptional, IsPositive, IsString, Matches } from "class-validator"; +import { IGDBGameOrderBy, IGDBSort } from "@/shared/infra/integrations/igdb.service"; export class SearchGameDto { @IsOptional() diff --git a/src/modules/game/game.module.ts b/src/modules/game/game.module.ts index 30ef99f..0b946d9 100644 --- a/src/modules/game/game.module.ts +++ b/src/modules/game/game.module.ts @@ -1,13 +1,12 @@ import { Module } from "@nestjs/common"; - +import { GameController } from "./controller/game.controller"; import { GameProgressController } from "./controller/game-progress.controller"; -import { GameProgressService } from "./service/game-progress.service"; import { GameReviewController } from "./controller/game-review.controller"; -import { GameReviewService } from "./service/game-review.service"; import { GameReviewScreenshotController } from "./controller/game-review-screenshot.controller"; -import { GameReviewScreenshotService } from "./service/game-review-screenshot.service"; -import { GameController } from "./controller/game.controller"; import { GameService } from "./service/game.service"; +import { GameProgressService } from "./service/game-progress.service"; +import { GameReviewService } from "./service/game-review.service"; +import { GameReviewScreenshotService } from "./service/game-review-screenshot.service"; @Module({ imports: [], diff --git a/src/modules/game/service/game-progress.service.ts b/src/modules/game/service/game-progress.service.ts index f8cd3be..1c2acb7 100644 --- a/src/modules/game/service/game-progress.service.ts +++ b/src/modules/game/service/game-progress.service.ts @@ -1,12 +1,12 @@ -import { DatabaseService } from "@/shared/infra/database/database.service"; import { Injectable } from "@nestjs/common"; -import { CreateOrUpdateGameProgressDto } from "../dto/create-or-update-game-progress.dto"; -import { GetGameProgressDto } from "../dto/get-game-progress.dto"; +import { FeedEventType } from "@prisma/generated/enums"; import { GameProgressFindManyArgs } from "@prisma/generated/models"; -import { AppException } from "@/shared/exceptions/app.exceptions"; import { ERROR_CODES } from "@/shared/constants/error-codes"; +import { AppException } from "@/shared/exceptions/app.exceptions"; +import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; -import { FeedEventType } from "@prisma/generated/enums"; +import { CreateOrUpdateGameProgressDto } from "../dto/create-or-update-game-progress.dto"; +import { GetGameProgressDto } from "../dto/get-game-progress.dto"; @Injectable() export class GameProgressService { diff --git a/src/modules/game/service/game-review.service.ts b/src/modules/game/service/game-review.service.ts index 1fd6c49..c73dddb 100644 --- a/src/modules/game/service/game-review.service.ts +++ b/src/modules/game/service/game-review.service.ts @@ -107,7 +107,11 @@ export class GameReviewService { where: { ...(getGameReviewsDto.gameId && { gameId: getGameReviewsDto.gameId }), ...(getGameReviewsDto.userId && { userId: getGameReviewsDto.userId }), + ...(getGameReviewsDto.query && { + game: { name: { contains: getGameReviewsDto.query, mode: "insensitive" } }, + }), }, + orderBy: { createdAt: "desc" }, include: { game: { select: { @@ -130,6 +134,19 @@ export class GameReviewService { }, }, }, + reactions: { + select: { + id: true, + emoji: true, + createdAt: true, + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, }, }); diff --git a/src/modules/game/service/game.service.ts b/src/modules/game/service/game.service.ts index cbcb73d..9c7d9c7 100644 --- a/src/modules/game/service/game.service.ts +++ b/src/modules/game/service/game.service.ts @@ -1,22 +1,19 @@ import { Injectable } from "@nestjs/common"; -import { Game, ProgressStatus } from "@prisma/generated/client"; +import { ProgressStatus } from "@prisma/generated/client"; import { GameCreateInput, GameUpdateInput } from "@prisma/generated/models"; import { TopGameDto } from "@/modules/game/dto/top-game.dto"; -import { CACHE_KEYS } from "@/shared/constants/cache"; import { ERROR_CODES } from "@/shared/constants/error-codes"; import { REFRESH_INTERVAL_MS } from "@/shared/constants/refresh-interval"; import { AppException } from "@/shared/exceptions/app.exceptions"; -import { CacheService } from "@/shared/infra/cache/cache.service"; import { DatabaseService } from "@/shared/infra/database/database.service"; +import { IGDBGameOrderBy, IGDBSort } from "@/shared/infra/integrations/igdb.service"; import { IntegrationsService } from "@/shared/infra/integrations/integrations.service"; import type { RefreshGameDto } from "../dto/refresh-game.dto"; import type { SearchGameDto } from "../dto/search-game.dto"; -import { IGDBGameOrderBy, IGDBSort } from "@/shared/infra/integrations/igdb.service"; @Injectable() export class GameService { constructor( - private readonly cacheService: CacheService, private readonly databaseService: DatabaseService, private readonly integrationsService: IntegrationsService, ) {} @@ -98,12 +95,6 @@ export class GameService { } async getGameByIgdbId(igdbId: number) { - const cachedGame = await this.cacheService.get(CACHE_KEYS.GAME_BY_IGDB_ID.prefix(igdbId)); - - if (cachedGame) { - return cachedGame; - } - let game = await this.databaseService.game.findUnique({ where: { igdbId }, }); @@ -150,12 +141,6 @@ export class GameService { progressStats, }; - await this.cacheService.set( - CACHE_KEYS.GAME_BY_IGDB_ID.prefix(igdbId), - gameWithStats, - CACHE_KEYS.GAME_BY_IGDB_ID.expiration, - ); - return gameWithStats; } @@ -175,10 +160,6 @@ export class GameService { throw new AppException(ERROR_CODES.GAME_ALREADY_REFRESHED); } - if (await this.cacheService.exists(CACHE_KEYS.GAME_BY_IGDB_ID.prefix(refreshGameDto.igdbId))) { - await this.cacheService.delete(CACHE_KEYS.GAME_BY_IGDB_ID.prefix(refreshGameDto.igdbId)); - } - const igdbGame = await this.integrationsService.igdb.getGameById(refreshGameDto.igdbId); await this.databaseService.game.update({ diff --git a/src/modules/list/controller/list.controller.ts b/src/modules/list/controller/list.controller.ts index 8fe3466..be7d3f9 100644 --- a/src/modules/list/controller/list.controller.ts +++ b/src/modules/list/controller/list.controller.ts @@ -7,18 +7,22 @@ import { HttpStatus, Param, ParseUUIDPipe, + Patch, Post, Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { AddItemToListDto } from "../dto/add-item-to-list.dto"; import { CreateListDto } from "../dto/create-list.dto"; import { GetItemsByListIdDto } from "../dto/get-items-by-list-id.dto"; +import { GetListStatusDto } from "../dto/get-list-status.dto"; import { GetListsByUserIdDto } from "../dto/get-lists-by-user-id.dto"; +import { GetListsContainingItemDto } from "../dto/get-lists-containing-item.dto"; import { RemoveItemFromListDto } from "../dto/remove-item-from-list.dto"; +import { UpdateListDto } from "../dto/update-list.dto"; import { ListService } from "../service/list.service"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("List") @Controller("/list") @@ -45,6 +49,24 @@ export class ListController { return { lists }; } + @Get("/status") + @UseGuards(AuthGuard) + async getListStatus(@Session() session: UserSession, @Query() query: GetListStatusDto) { + const listIds = await this.listService.getListStatus({ + ...query, + userId: session.user.id, + }); + + return { listIds }; + } + + @Get("/containing") + async getListsContainingItem(@Query() query: GetListsContainingItemDto) { + const lists = await this.listService.getListsContainingItem(query); + + return { lists }; + } + @Get("/:listId") async getListById(@Param("listId", new ParseUUIDPipe()) listId: string) { const list = await this.listService.getListById(listId); @@ -90,4 +112,28 @@ export class ListController { listId, }); } + + @Patch("/:listId") + @UseGuards(AuthGuard) + async updateList( + @Param("listId", new ParseUUIDPipe()) listId: string, + @Session() session: UserSession, + @Body() body: UpdateListDto, + ) { + await this.listService.updateList({ + ...body, + userId: session.user.id, + listId, + }); + } + + @Delete("/:listId") + @UseGuards(AuthGuard) + @HttpCode(HttpStatus.NO_CONTENT) + async deleteList(@Param("listId", new ParseUUIDPipe()) listId: string, @Session() session: UserSession) { + await this.listService.deleteList({ + listId, + userId: session.user.id, + }); + } } diff --git a/src/modules/list/dto/add-item-to-list.dto.ts b/src/modules/list/dto/add-item-to-list.dto.ts index 6df1a31..175a9c0 100644 --- a/src/modules/list/dto/add-item-to-list.dto.ts +++ b/src/modules/list/dto/add-item-to-list.dto.ts @@ -1,62 +1,41 @@ -import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ListType } from "@prisma/generated/enums"; import { Type } from "class-transformer"; -import { IsEnum, IsNotEmpty, IsOptional, IsPositive, IsUUID, ValidateNested } from "class-validator"; +import { + IsEnum, + IsOptional, + IsPositive, + IsUUID, + registerDecorator, + ValidationArguments, + ValidationOptions, +} from "class-validator"; -export class AnimeItemDto { - @ApiProperty({ type: "string", format: "uuid", description: "Required when type is Anime" }) - @IsNotEmpty({ message: "animeId is required when type is Anime" }) - @IsUUID("4", { message: "animeId must be a valid UUID" }) - readonly animeId: string; +export function ListRequiredForType(favoriteType: ListType, options?: ValidationOptions) { + return (object: object, propertyName: string) => { + registerDecorator({ + name: "favoriteRequiredForType", + target: object.constructor, + propertyName, + constraints: [favoriteType], + options, + validator: { + validate(value: unknown, args: ValidationArguments) { + const dto = args.object as AddItemToListDto; + if (dto.type === favoriteType) { + return value !== undefined && value !== null && value !== ""; + } + return true; + }, + defaultMessage(args: ValidationArguments) { + const [type] = args.constraints as [ListType]; + return `${args.property} is required when type is ${type}`; + }, + }, + }); + }; } -export class MangaItemDto { - @ApiProperty({ type: "string", format: "uuid", description: "Required when type is Manga" }) - @IsNotEmpty({ message: "mangaId is required when type is Manga" }) - @IsUUID("4", { message: "mangaId must be a valid UUID" }) - readonly mangaId: string; -} - -export class TVShowItemDto { - @ApiProperty({ type: "string", format: "uuid", description: "Required when type is TVShow" }) - @IsNotEmpty({ message: "tvShowId is required when type is TVShow" }) - @IsUUID("4", { message: "tvShowId must be a valid UUID" }) - readonly tvShowId: string; -} - -export class MovieItemDto { - @ApiProperty({ type: "string", format: "uuid", description: "Required when type is Movie" }) - @IsNotEmpty({ message: "movieId is required when type is Movie" }) - @IsUUID("4", { message: "movieId must be a valid UUID" }) - readonly movieId: string; -} - -export class GameItemDto { - @ApiProperty({ type: "string", format: "uuid", description: "Required when type is Game" }) - @IsNotEmpty({ message: "gameId is required when type is Game" }) - @IsUUID("4", { message: "gameId must be a valid UUID" }) - readonly gameId: string; -} - -export class BookItemDto { - @ApiProperty({ type: "string", format: "uuid", description: "Required when type is Book" }) - @IsNotEmpty({ message: "bookId is required when type is Book" }) - @IsUUID("4", { message: "bookId must be a valid UUID" }) - readonly bookId: string; -} - -type ItemDto = AnimeItemDto | MangaItemDto | TVShowItemDto | MovieItemDto | GameItemDto | BookItemDto; - -const itemTypeMap: Partial ItemDto>> = { - [ListType.Anime]: AnimeItemDto, - [ListType.Manga]: MangaItemDto, - [ListType.TVShow]: TVShowItemDto, - [ListType.Movie]: MovieItemDto, - [ListType.Game]: GameItemDto, - [ListType.Book]: BookItemDto, -}; - -@ApiExtraModels(AnimeItemDto, MangaItemDto, TVShowItemDto, MovieItemDto, GameItemDto, BookItemDto) export class AddItemToListDto { @IsEnum(ListType) @ApiProperty({ enum: ListType }) @@ -75,17 +54,39 @@ export class AddItemToListDto { @ApiProperty({ type: "string" }) readonly userId: string; - @ValidateNested() - @Type((options) => itemTypeMap[(options?.object as AddItemToListDto).type] ?? Object) - @ApiProperty({ - oneOf: [ - { $ref: "#/components/schemas/AnimeItemDto" }, - { $ref: "#/components/schemas/MangaItemDto" }, - { $ref: "#/components/schemas/TVShowItemDto" }, - { $ref: "#/components/schemas/MovieItemDto" }, - { $ref: "#/components/schemas/GameItemDto" }, - { $ref: "#/components/schemas/BookItemDto" }, - ], - }) - readonly item: ItemDto; + @IsOptional() + @ListRequiredForType(ListType.Anime) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Anime" }) + readonly animeId?: string; + + @IsOptional() + @ListRequiredForType(ListType.Manga) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Manga" }) + readonly mangaId?: string; + + @IsOptional() + @ListRequiredForType(ListType.TVShow) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is TVShow" }) + readonly tvShowId?: string; + + @IsOptional() + @ListRequiredForType(ListType.Movie) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Movie" }) + readonly movieId?: string; + + @IsOptional() + @ListRequiredForType(ListType.Game) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Game" }) + readonly gameId?: string; + + @IsOptional() + @ListRequiredForType(ListType.Book) + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid", description: "Required when type is Book" }) + readonly bookId?: string; } diff --git a/src/modules/list/dto/get-items-by-list-id.dto.ts b/src/modules/list/dto/get-items-by-list-id.dto.ts index 3c89d00..5f99c3b 100644 --- a/src/modules/list/dto/get-items-by-list-id.dto.ts +++ b/src/modules/list/dto/get-items-by-list-id.dto.ts @@ -1,9 +1,10 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsUUID } from "class-validator"; +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsOptional, IsUUID } from "class-validator"; import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetItemsByListIdDto extends OffsetPaginationParamsDto { + @IsOptional() @IsUUID("7", { message: "listId must be a valid UUID" }) - @ApiProperty({ type: "string", format: "uuid" }) - readonly listId: string; + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly listId?: string; } diff --git a/src/modules/list/dto/get-list-status.dto.ts b/src/modules/list/dto/get-list-status.dto.ts new file mode 100644 index 0000000..a99ccc3 --- /dev/null +++ b/src/modules/list/dto/get-list-status.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { ListType } from "@prisma/generated/enums"; +import { IsEnum, IsOptional, IsUUID } from "class-validator"; + +export class GetListStatusDto { + @IsEnum(ListType) + @ApiProperty({ enum: ListType }) + readonly type: ListType; + + @ApiProperty({ type: "string" }) + readonly userId: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly animeId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly mangaId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly tvShowId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly movieId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly gameId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly bookId?: string; +} diff --git a/src/modules/list/dto/get-lists-by-user-id.dto.ts b/src/modules/list/dto/get-lists-by-user-id.dto.ts index 90beb23..de1e177 100644 --- a/src/modules/list/dto/get-lists-by-user-id.dto.ts +++ b/src/modules/list/dto/get-lists-by-user-id.dto.ts @@ -1,10 +1,22 @@ -import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; +import { ApiPropertyOptional } from "@nestjs/swagger"; import { ListType } from "@prisma/generated/enums"; -import { IsEnum } from "class-validator"; +import { IsEnum, IsOptional, IsString } from "class-validator"; +import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetListsByUserIdDto extends OffsetPaginationParamsDto { + @IsOptional() @IsEnum(ListType) - readonly type: ListType; + @ApiPropertyOptional({ enum: ListType }) + readonly type?: ListType; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: "Search query to filter lists by name", + example: "favorites", + type: "string", + }) + readonly query?: string; readonly userId: string; } diff --git a/src/modules/list/dto/get-lists-containing-item.dto.ts b/src/modules/list/dto/get-lists-containing-item.dto.ts new file mode 100644 index 0000000..f303dcc --- /dev/null +++ b/src/modules/list/dto/get-lists-containing-item.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { ListType } from "@prisma/generated/enums"; +import { IsEnum, IsOptional, IsUUID } from "class-validator"; +import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; + +export class GetListsContainingItemDto extends OffsetPaginationParamsDto { + @IsEnum(ListType) + @ApiProperty({ enum: ListType }) + readonly type: ListType; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly animeId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly mangaId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly tvShowId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly movieId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly gameId?: string; + + @IsOptional() + @IsUUID() + @ApiPropertyOptional({ type: "string", format: "uuid" }) + readonly bookId?: string; +} diff --git a/src/modules/list/dto/update-list.dto.ts b/src/modules/list/dto/update-list.dto.ts new file mode 100644 index 0000000..d898dda --- /dev/null +++ b/src/modules/list/dto/update-list.dto.ts @@ -0,0 +1,19 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional, IsString } from "class-validator"; + +export class UpdateListDto { + @IsOptional() + @IsNotEmpty() + @IsString() + @ApiPropertyOptional({ type: "string" }) + readonly name?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: "string" }) + readonly description?: string; + + readonly listId: string; + + readonly userId: string; +} diff --git a/src/modules/list/service/list.service.ts b/src/modules/list/service/list.service.ts index b015101..703ba5b 100644 --- a/src/modules/list/service/list.service.ts +++ b/src/modules/list/service/list.service.ts @@ -9,8 +9,11 @@ import { AddItemToListDto } from "../dto/add-item-to-list.dto"; import { CreateListDto } from "../dto/create-list.dto"; import { DeleteListDto } from "../dto/delete-list.dto"; import { GetItemsByListIdDto } from "../dto/get-items-by-list-id.dto"; +import { GetListStatusDto } from "../dto/get-list-status.dto"; import { GetListsByUserIdDto } from "../dto/get-lists-by-user-id.dto"; +import { GetListsContainingItemDto } from "../dto/get-lists-containing-item.dto"; import { RemoveItemFromListDto } from "../dto/remove-item-from-list.dto"; +import { UpdateListDto } from "../dto/update-list.dto"; @Injectable() export class ListService { @@ -56,7 +59,7 @@ export class ListService { } async addItemToList(addItemToListDto: AddItemToListDto) { - const { listId, userId, item, position } = addItemToListDto; + const { listId, userId, position, type: _, ...entityIds } = addItemToListDto; const listAlreadyExists = await this.databaseService.list.findFirst({ where: { @@ -69,12 +72,10 @@ export class ListService { throw new AppException(ERROR_CODES.LIST_NOT_FOUND); } - const entityId = { ...item } as Record; - const listItemAlreadyExists = await this.databaseService.listItem.findFirst({ where: { listId, - ...entityId, + ...entityIds, }, }); @@ -86,7 +87,7 @@ export class ListService { data: { listId, position, - ...entityId, + ...entityIds, }, include: { anime: { @@ -200,7 +201,29 @@ export class ListService { model: "list", itemsPerPage: getListsByUserIdDto.itemsPerPage, page: getListsByUserIdDto.page, - where: { userId: getListsByUserIdDto.userId, type: getListsByUserIdDto.type }, + where: { + userId: getListsByUserIdDto.userId, + type: getListsByUserIdDto.type, + ...(getListsByUserIdDto.query && { + OR: [ + { name: { contains: getListsByUserIdDto.query, mode: "insensitive" as const } }, + { + listItems: { + some: { + OR: [ + { anime: { title: { contains: getListsByUserIdDto.query, mode: "insensitive" as const } } }, + { manga: { title: { contains: getListsByUserIdDto.query, mode: "insensitive" as const } } }, + { tvShow: { name: { contains: getListsByUserIdDto.query, mode: "insensitive" as const } } }, + { movie: { title: { contains: getListsByUserIdDto.query, mode: "insensitive" as const } } }, + { game: { name: { contains: getListsByUserIdDto.query, mode: "insensitive" as const } } }, + { book: { title: { contains: getListsByUserIdDto.query, mode: "insensitive" as const } } }, + ], + }, + }, + }, + ], + }), + }, include: { user: { select: { @@ -215,12 +238,82 @@ export class ListService { }, }, }, + _count: { + select: { listItems: true }, + }, + listItems: { + take: 4, + include: { + anime: { + select: { + id: true, + malId: true, + imageUrl: true, + title: true, + }, + }, + manga: { + select: { + id: true, + malId: true, + imageUrl: true, + title: true, + }, + }, + tvShow: { + select: { + id: true, + tmdbId: true, + backdropUrl: true, + name: true, + }, + }, + book: { + select: { + id: true, + hardcoverId: true, + imageUrl: true, + title: true, + }, + }, + game: { + select: { + id: true, + igdbId: true, + coverUrl: true, + name: true, + }, + }, + movie: { + select: { + id: true, + tmdbId: true, + backdropUrl: true, + title: true, + }, + }, + }, + }, }, }); return lists; } + async getListStatus(getListStatusDto: GetListStatusDto & { userId: string }) { + const { type, userId, ...entityIds } = getListStatusDto; + + const listItems = await this.databaseService.listItem.findMany({ + where: { + ...entityIds, + list: { userId, type }, + }, + select: { listId: true }, + }); + + return listItems.map((listItem) => listItem.listId); + } + async getItemsByListId(getItemsByListIdDto: GetItemsByListIdDto) { const listAlreadyExists = await this.databaseService.list.findUnique({ where: { id: getItemsByListIdDto.listId }, @@ -309,8 +402,95 @@ export class ListService { return listItems; } + async getListsContainingItem(getListsContainingItemDto: GetListsContainingItemDto) { + const { type, page, itemsPerPage, ...entityIds } = getListsContainingItemDto; + + const lists = await this.databaseService.offsetPagination({ + model: "list", + itemsPerPage, + page, + where: { + type, + listItems: { some: { ...entityIds } }, + }, + include: { + user: { + select: { + id: true, + name: true, + username: true, + profile: { + select: { + id: true, + avatarUrl: true, + }, + }, + }, + }, + _count: { + select: { listItems: true }, + }, + listItems: { + take: 4, + include: { + anime: { + select: { + id: true, + malId: true, + imageUrl: true, + title: true, + }, + }, + manga: { + select: { + id: true, + malId: true, + imageUrl: true, + title: true, + }, + }, + tvShow: { + select: { + id: true, + tmdbId: true, + backdropUrl: true, + name: true, + }, + }, + book: { + select: { + id: true, + hardcoverId: true, + imageUrl: true, + title: true, + }, + }, + game: { + select: { + id: true, + igdbId: true, + coverUrl: true, + name: true, + }, + }, + movie: { + select: { + id: true, + tmdbId: true, + backdropUrl: true, + title: true, + }, + }, + }, + }, + }, + }); + + return lists; + } + async removeItemFromList(removeItemFromListDto: RemoveItemFromListDto) { - const { listId, userId, item } = removeItemFromListDto; + const { listId, userId, type: _, ...entityIds } = removeItemFromListDto; const list = await this.databaseService.list.findFirst({ where: { @@ -323,12 +503,10 @@ export class ListService { throw new AppException(ERROR_CODES.LIST_NOT_FOUND); } - const entityId = { ...item } as Record; - const listItem = await this.databaseService.listItem.findFirst({ where: { listId, - ...entityId, + ...entityIds, }, }); @@ -363,4 +541,29 @@ export class ListService { }, }); } + + async updateList(updateListDto: UpdateListDto) { + const { listId, userId, name, description } = updateListDto; + + const list = await this.databaseService.list.findFirst({ + where: { + id: listId, + userId, + }, + }); + + if (!list) { + throw new AppException(ERROR_CODES.LIST_NOT_FOUND); + } + + await this.databaseService.list.update({ + where: { + id: listId, + }, + data: { + name, + description, + }, + }); + } } diff --git a/src/modules/manga/controller/manga-progress.controller.ts b/src/modules/manga/controller/manga-progress.controller.ts index 4843e45..c1decbd 100644 --- a/src/modules/manga/controller/manga-progress.controller.ts +++ b/src/modules/manga/controller/manga-progress.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { MangaProgressService } from "../service/manga-progress.service"; import { CreateOrUpdateMangaProgressDto } from "../dto/create-or-update-manga-progress.dto"; import { GetMangaProgressDto } from "../dto/get-manga-progressesdto"; -import { ApiTags } from "@nestjs/swagger"; +import { MangaProgressService } from "../service/manga-progress.service"; @ApiTags("Manga") @Controller("/manga/progress") diff --git a/src/modules/manga/controller/manga-review.controller.ts b/src/modules/manga/controller/manga-review.controller.ts index e7b98a1..d5d4615 100644 --- a/src/modules/manga/controller/manga-review.controller.ts +++ b/src/modules/manga/controller/manga-review.controller.ts @@ -12,12 +12,12 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { CreateMangaReviewDto } from "../dto/create-manga-review.dto"; import { GetMangaReviewsDto } from "../dto/get-manga-reviews.dto"; import { UpdateMangaReviewDto } from "../dto/update-manga-review.dto"; import { MangaReviewService } from "../service/manga-review.service"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("Manga") @Controller("/manga/review") diff --git a/src/modules/manga/dto/get-manga-progressesdto.ts b/src/modules/manga/dto/get-manga-progressesdto.ts index 7b1e501..ac22551 100644 --- a/src/modules/manga/dto/get-manga-progressesdto.ts +++ b/src/modules/manga/dto/get-manga-progressesdto.ts @@ -1,5 +1,5 @@ -import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; import { IsOptional, IsUUID } from "class-validator"; +import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetMangaProgressDto extends OffsetPaginationParamsDto { @IsOptional() diff --git a/src/modules/manga/dto/get-manga-reviews.dto.ts b/src/modules/manga/dto/get-manga-reviews.dto.ts index d93bfd4..4964c4d 100644 --- a/src/modules/manga/dto/get-manga-reviews.dto.ts +++ b/src/modules/manga/dto/get-manga-reviews.dto.ts @@ -1,4 +1,4 @@ -import { IsOptional, IsUUID } from "class-validator"; +import { IsOptional, IsString, IsUUID } from "class-validator"; import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetMangaReviewsDto extends OffsetPaginationParamsDto { @@ -9,4 +9,8 @@ export class GetMangaReviewsDto extends OffsetPaginationParamsDto { @IsOptional() @IsUUID() readonly userId?: string; + + @IsOptional() + @IsString() + readonly query?: string; } diff --git a/src/modules/manga/dto/search-manga.dto.ts b/src/modules/manga/dto/search-manga.dto.ts index 5119996..d7036e4 100644 --- a/src/modules/manga/dto/search-manga.dto.ts +++ b/src/modules/manga/dto/search-manga.dto.ts @@ -1,11 +1,11 @@ +import { Transform, Type } from "class-transformer"; +import { IsArray, IsEnum, IsInt, IsOptional, IsPositive, Matches } from "class-validator"; import { - JikanMangaType, + JikanMangaOrderBy, JikanMangaStatus, + JikanMangaType, JikanSort, - JikanMangaOrderBy, } from "@/shared/infra/integrations/jikan.service"; -import { Transform, Type } from "class-transformer"; -import { IsArray, IsEnum, IsInt, IsOptional, IsPositive, Matches } from "class-validator"; export class SearchMangaDto { @IsOptional() diff --git a/src/modules/manga/dto/top-manga.dto.ts b/src/modules/manga/dto/top-manga.dto.ts index f5c97b0..540009c 100644 --- a/src/modules/manga/dto/top-manga.dto.ts +++ b/src/modules/manga/dto/top-manga.dto.ts @@ -1,6 +1,6 @@ -import { JikanMangaType, JikanMangaFilter } from "@/shared/infra/integrations/jikan.service"; import { Type } from "class-transformer"; import { IsEnum, IsInt, IsOptional, IsPositive } from "class-validator"; +import { JikanMangaFilter, JikanMangaType } from "@/shared/infra/integrations/jikan.service"; export class TopMangaDto { @Type(() => Number) diff --git a/src/modules/manga/manga.module.ts b/src/modules/manga/manga.module.ts index f6457b8..ba33975 100644 --- a/src/modules/manga/manga.module.ts +++ b/src/modules/manga/manga.module.ts @@ -1,11 +1,10 @@ import { Module } from "@nestjs/common"; - +import { MangaController } from "./controller/manga.controller"; import { MangaProgressController } from "./controller/manga-progress.controller"; -import { MangaProgressService } from "./service/manga-progress.service"; import { MangaReviewController } from "./controller/manga-review.controller"; -import { MangaReviewService } from "./service/manga-review.service"; -import { MangaController } from "./controller/manga.controller"; import { MangaService } from "./service/manga.service"; +import { MangaProgressService } from "./service/manga-progress.service"; +import { MangaReviewService } from "./service/manga-review.service"; @Module({ imports: [], diff --git a/src/modules/manga/service/manga-progress.service.ts b/src/modules/manga/service/manga-progress.service.ts index cf2f381..778d420 100644 --- a/src/modules/manga/service/manga-progress.service.ts +++ b/src/modules/manga/service/manga-progress.service.ts @@ -1,12 +1,12 @@ -import { DatabaseService } from "@/shared/infra/database/database.service"; import { Injectable } from "@nestjs/common"; -import { CreateOrUpdateMangaProgressDto } from "../dto/create-or-update-manga-progress.dto"; -import { AppException } from "@/shared/exceptions/app.exceptions"; -import { ERROR_CODES } from "@/shared/constants/error-codes"; -import { GetMangaProgressDto } from "../dto/get-manga-progressesdto"; +import { FeedEventType } from "@prisma/generated/enums"; import { MangaProgressFindManyArgs } from "@prisma/generated/models"; +import { ERROR_CODES } from "@/shared/constants/error-codes"; +import { AppException } from "@/shared/exceptions/app.exceptions"; +import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; -import { FeedEventType } from "@prisma/generated/enums"; +import { CreateOrUpdateMangaProgressDto } from "../dto/create-or-update-manga-progress.dto"; +import { GetMangaProgressDto } from "../dto/get-manga-progressesdto"; @Injectable() export class MangaProgressService { diff --git a/src/modules/manga/service/manga-review.service.ts b/src/modules/manga/service/manga-review.service.ts index c1794f0..e9a512d 100644 --- a/src/modules/manga/service/manga-review.service.ts +++ b/src/modules/manga/service/manga-review.service.ts @@ -106,7 +106,11 @@ export class MangaReviewService { where: { ...(getMangaReviewsDto.mangaId && { mangaId: getMangaReviewsDto.mangaId }), ...(getMangaReviewsDto.userId && { userId: getMangaReviewsDto.userId }), + ...(getMangaReviewsDto.query && { + manga: { title: { contains: getMangaReviewsDto.query, mode: "insensitive" } }, + }), }, + orderBy: { createdAt: "desc" }, include: { manga: { select: { @@ -129,6 +133,19 @@ export class MangaReviewService { }, }, }, + reactions: { + select: { + id: true, + emoji: true, + createdAt: true, + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, }, }); diff --git a/src/modules/manga/service/manga.service.ts b/src/modules/manga/service/manga.service.ts index b05b9da..e19674d 100644 --- a/src/modules/manga/service/manga.service.ts +++ b/src/modules/manga/service/manga.service.ts @@ -1,11 +1,9 @@ import { Injectable } from "@nestjs/common"; -import { Manga, ProgressStatus } from "@prisma/generated/client"; +import { ProgressStatus } from "@prisma/generated/client"; import { MangaCreateInput, MangaUpdateInput } from "@prisma/generated/models"; -import { CACHE_KEYS } from "@/shared/constants/cache"; import { ERROR_CODES } from "@/shared/constants/error-codes"; import { REFRESH_INTERVAL_MS } from "@/shared/constants/refresh-interval"; import { AppException } from "@/shared/exceptions/app.exceptions"; -import { CacheService } from "@/shared/infra/cache/cache.service"; import { DatabaseService } from "@/shared/infra/database/database.service"; import { IntegrationsService } from "@/shared/infra/integrations/integrations.service"; import { @@ -21,7 +19,6 @@ import { TopMangaDto } from "../dto/top-manga.dto"; @Injectable() export class MangaService { constructor( - private readonly cacheService: CacheService, private readonly databaseService: DatabaseService, private readonly integrationsService: IntegrationsService, ) {} @@ -101,14 +98,6 @@ export class MangaService { } async getMangaByMalId(malId: number) { - const mangaDetailKey = CACHE_KEYS.MANGA_BY_MAL_ID.prefix(malId); - - const cachedManga = await this.cacheService.get(mangaDetailKey); - - if (cachedManga) { - return cachedManga; - } - let manga = await this.databaseService.manga.findUnique({ where: { malId }, }); @@ -155,19 +144,10 @@ export class MangaService { progressStats, }; - await this.cacheService.set(mangaDetailKey, mangaWithStats, CACHE_KEYS.MANGA_BY_MAL_ID.expiration); - return mangaWithStats; } async getMangaRelationsByMalId(malId: number) { - const cachedRelationsKey = CACHE_KEYS.MANGA_RELATIONS_BY_MAL_ID.prefix(malId); - const cachedRelations = await this.cacheService.get(cachedRelationsKey); - - if (cachedRelations) { - return cachedRelations; - } - const manga = await this.databaseService.manga.findUnique({ where: { malId }, select: { relations: true }, @@ -178,15 +158,11 @@ export class MangaService { } if (manga.relations) { - await this.cacheService.set(cachedRelationsKey, manga.relations, CACHE_KEYS.MANGA_RELATIONS_BY_MAL_ID.expiration); - return manga.relations; } const relations = await this.integrationsService.jikan.getMangaRelationsById(malId); - await this.cacheService.set(cachedRelationsKey, relations, CACHE_KEYS.MANGA_RELATIONS_BY_MAL_ID.expiration); - await this.databaseService.manga.update({ where: { malId }, data: { relations }, @@ -211,12 +187,6 @@ export class MangaService { throw new AppException(ERROR_CODES.MANGA_ALREADY_REFRESHED); } - const mangaDetailKey = CACHE_KEYS.MANGA_BY_MAL_ID.prefix(refreshMangaDto.malId); - - if (await this.cacheService.exists(mangaDetailKey)) { - await this.cacheService.delete(mangaDetailKey); - } - const jikanManga = await this.integrationsService.jikan.getMangaById(refreshMangaDto.malId); const jikanRelations = await this.integrationsService.jikan.getMangaRelationsById(refreshMangaDto.malId); diff --git a/src/modules/movie/controller/movie-progress.controller.ts b/src/modules/movie/controller/movie-progress.controller.ts index 6044226..9c543a8 100644 --- a/src/modules/movie/controller/movie-progress.controller.ts +++ b/src/modules/movie/controller/movie-progress.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { MovieProgressService } from "../service/movie-progress.service"; import { CreateOrUpdateMovieProgressDto } from "../dto/create-or-update-movie-progress.dto"; import { GetMovieProgressDto } from "../dto/get-movie-progress.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { MovieProgressService } from "../service/movie-progress.service"; @ApiTags("Movie") @Controller("/movie/progress") diff --git a/src/modules/movie/controller/movie-review.controller.ts b/src/modules/movie/controller/movie-review.controller.ts index 8db8411..08f7c87 100644 --- a/src/modules/movie/controller/movie-review.controller.ts +++ b/src/modules/movie/controller/movie-review.controller.ts @@ -12,11 +12,11 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { CreateMovieReviewDto } from "../dto/create-movie-review.dto"; import { GetMovieReviewsDto } from "../dto/get-movie-reviews.dto"; import { MovieReviewService } from "../service/movie-review.service"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("Movie") @Controller("/movie/review") diff --git a/src/modules/movie/dto/get-movie-reviews.dto.ts b/src/modules/movie/dto/get-movie-reviews.dto.ts index 4e738b6..8bb5bf5 100644 --- a/src/modules/movie/dto/get-movie-reviews.dto.ts +++ b/src/modules/movie/dto/get-movie-reviews.dto.ts @@ -1,5 +1,5 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional, IsUUID } from "class-validator"; +import { IsOptional, IsString, IsUUID } from "class-validator"; import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetMovieReviewsDto extends OffsetPaginationParamsDto { @@ -20,4 +20,13 @@ export class GetMovieReviewsDto extends OffsetPaginationParamsDto { type: "string", }) readonly userId?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: "Search query to filter reviews by media title", + example: "matrix", + type: "string", + }) + readonly query?: string; } diff --git a/src/modules/movie/movie.module.ts b/src/modules/movie/movie.module.ts index 5517955..bf3b8c5 100644 --- a/src/modules/movie/movie.module.ts +++ b/src/modules/movie/movie.module.ts @@ -1,11 +1,10 @@ import { Module } from "@nestjs/common"; - +import { MovieController } from "./controller/movie.controller"; import { MovieProgressController } from "./controller/movie-progress.controller"; -import { MovieProgressService } from "./service/movie-progress.service"; import { MovieReviewController } from "./controller/movie-review.controller"; -import { MovieReviewService } from "./service/movie-review.service"; -import { MovieController } from "./controller/movie.controller"; import { MovieService } from "./service/movie.service"; +import { MovieProgressService } from "./service/movie-progress.service"; +import { MovieReviewService } from "./service/movie-review.service"; @Module({ imports: [], diff --git a/src/modules/movie/service/movie-progress.service.ts b/src/modules/movie/service/movie-progress.service.ts index 45a859c..f48e07d 100644 --- a/src/modules/movie/service/movie-progress.service.ts +++ b/src/modules/movie/service/movie-progress.service.ts @@ -1,12 +1,12 @@ -import { DatabaseService } from "@/shared/infra/database/database.service"; import { Injectable } from "@nestjs/common"; -import { CreateOrUpdateMovieProgressDto } from "../dto/create-or-update-movie-progress.dto"; -import { GetMovieProgressDto } from "../dto/get-movie-progress.dto"; +import { FeedEventType } from "@prisma/generated/enums"; import { MovieProgressFindManyArgs } from "@prisma/generated/models"; -import { AppException } from "@/shared/exceptions/app.exceptions"; import { ERROR_CODES } from "@/shared/constants/error-codes"; +import { AppException } from "@/shared/exceptions/app.exceptions"; +import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; -import { FeedEventType } from "@prisma/generated/enums"; +import { CreateOrUpdateMovieProgressDto } from "../dto/create-or-update-movie-progress.dto"; +import { GetMovieProgressDto } from "../dto/get-movie-progress.dto"; @Injectable() export class MovieProgressService { diff --git a/src/modules/movie/service/movie-review.service.ts b/src/modules/movie/service/movie-review.service.ts index 36ff456..f82746c 100644 --- a/src/modules/movie/service/movie-review.service.ts +++ b/src/modules/movie/service/movie-review.service.ts @@ -108,7 +108,11 @@ export class MovieReviewService { where: { ...(getMovieReviewsDto.userId && { userId: getMovieReviewsDto.userId }), ...(getMovieReviewsDto.movieId && { movieId: getMovieReviewsDto.movieId }), + ...(getMovieReviewsDto.query && { + movie: { title: { contains: getMovieReviewsDto.query, mode: "insensitive" } }, + }), }, + orderBy: { createdAt: "desc" }, include: { movie: { select: { @@ -132,6 +136,19 @@ export class MovieReviewService { }, }, }, + reactions: { + select: { + id: true, + emoji: true, + createdAt: true, + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, }, }); diff --git a/src/modules/movie/service/movie.service.ts b/src/modules/movie/service/movie.service.ts index 8111b07..2889a01 100644 --- a/src/modules/movie/service/movie.service.ts +++ b/src/modules/movie/service/movie.service.ts @@ -1,22 +1,19 @@ import { Injectable } from "@nestjs/common"; -import { Movie, ProgressStatus } from "@prisma/generated/client"; +import { ProgressStatus } from "@prisma/generated/client"; import { MovieCreateInput, MovieUpdateInput } from "@prisma/generated/models"; import { TopMovieDto } from "@/modules/movie/dto/top-movie.dto"; -import { CACHE_KEYS } from "@/shared/constants/cache"; import { ERROR_CODES } from "@/shared/constants/error-codes"; import { REFRESH_INTERVAL_MS } from "@/shared/constants/refresh-interval"; import { AppException } from "@/shared/exceptions/app.exceptions"; -import { CacheService } from "@/shared/infra/cache/cache.service"; import { DatabaseService } from "@/shared/infra/database/database.service"; import { IntegrationsService } from "@/shared/infra/integrations/integrations.service"; +import { TMDBMovieOrderBy, TMDBSort } from "@/shared/infra/integrations/tmdb.service"; import { RefreshMovieDto } from "../dto/refresh-movie.dto"; import type { SearchMovieDto } from "../dto/search-movie.dto"; -import { TMDBMovieOrderBy, TMDBSort } from "@/shared/infra/integrations/tmdb.service"; @Injectable() export class MovieService { constructor( - private readonly cacheService: CacheService, private readonly databaseService: DatabaseService, private readonly integrationsService: IntegrationsService, ) {} @@ -92,12 +89,6 @@ export class MovieService { } async getMovieByTmdbId(tmdbId: number) { - const cachedMovie = await this.cacheService.get(CACHE_KEYS.MOVIE_BY_IMDB_ID.prefix(tmdbId)); - - if (cachedMovie) { - return cachedMovie; - } - let movie = await this.databaseService.movie.findUnique({ where: { tmdbId }, }); @@ -144,12 +135,6 @@ export class MovieService { progressStats, }; - await this.cacheService.set( - CACHE_KEYS.MOVIE_BY_IMDB_ID.prefix(tmdbId), - movieWithStats, - CACHE_KEYS.MOVIE_BY_IMDB_ID.expiration, - ); - return movieWithStats; } @@ -166,10 +151,6 @@ export class MovieService { throw new AppException(ERROR_CODES.MOVIE_ALREADY_REFRESHED); } - if (await this.cacheService.exists(CACHE_KEYS.MOVIE_BY_IMDB_ID.prefix(movie.tmdbId))) { - await this.cacheService.delete(CACHE_KEYS.MOVIE_BY_IMDB_ID.prefix(movie.tmdbId)); - } - const tmdbMovie = await this.integrationsService.tmdb.getMovieById(movie.tmdbId); await this.databaseService.movie.update({ diff --git a/src/modules/payment/controller/payment.controller.ts b/src/modules/payment/controller/payment.controller.ts index 727eaaf..84fd814 100644 --- a/src/modules/payment/controller/payment.controller.ts +++ b/src/modules/payment/controller/payment.controller.ts @@ -1,10 +1,10 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from "@nestjs/common"; -import { PaymentService } from "../service/payment.service"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; +import { ClientIp, type ClientIpType } from "@/shared/decorators/client-ip.decorator"; import { CreatePaymentDto } from "../dto/create-payment.dto"; import { GetPaymentsDto } from "../dto/get-payments.dto"; -import { ClientIp, type ClientIpType } from "@/shared/decorators/client-ip.decorator"; +import { PaymentService } from "../service/payment.service"; @ApiTags("Payment") @Controller("/payment") diff --git a/src/modules/payment/controller/perk.controller.ts b/src/modules/payment/controller/perk.controller.ts index 0b3c6e7..ea36ea3 100644 --- a/src/modules/payment/controller/perk.controller.ts +++ b/src/modules/payment/controller/perk.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get } from "@nestjs/common"; import { ApiTags } from "@nestjs/swagger"; -import { PerkService } from "../service/perk.service"; import { ClientIp, type ClientIpType } from "@/shared/decorators/client-ip.decorator"; +import { PerkService } from "../service/perk.service"; @ApiTags("Payment") @Controller("/perk") diff --git a/src/modules/payment/controller/stripe.controller.ts b/src/modules/payment/controller/stripe.controller.ts index c0aebd8..f7840f6 100644 --- a/src/modules/payment/controller/stripe.controller.ts +++ b/src/modules/payment/controller/stripe.controller.ts @@ -5,20 +5,20 @@ import { Headers, Logger, Post, + type RawBodyRequest, Req, Session, UseGuards, - type RawBodyRequest, } from "@nestjs/common"; -import { StripeService } from "../service/stripe.service"; -import { ApiTags } from "@nestjs/swagger"; import { ConfigService } from "@nestjs/config"; +import { ApiTags } from "@nestjs/swagger"; +import { AuthGuard, type UserSession } from "@thallesp/nestjs-better-auth"; import Stripe from "stripe"; -import { AppException } from "@/shared/exceptions/app.exceptions"; import { ERROR_CODES } from "@/shared/constants/error-codes"; -import { AuthGuard, type UserSession } from "@thallesp/nestjs-better-auth"; import { ClientIp, type ClientIpType } from "@/shared/decorators/client-ip.decorator"; +import { AppException } from "@/shared/exceptions/app.exceptions"; import { getUserCurrency } from "@/shared/utils/currency"; +import { StripeService } from "../service/stripe.service"; @ApiTags("Payment") @Controller("/stripe") diff --git a/src/modules/payment/dto/create-payment.dto.ts b/src/modules/payment/dto/create-payment.dto.ts index 596045c..189aa19 100644 --- a/src/modules/payment/dto/create-payment.dto.ts +++ b/src/modules/payment/dto/create-payment.dto.ts @@ -1,7 +1,7 @@ -import type { ClientIpType } from "@/shared/decorators/client-ip.decorator"; import { PaymentFrequency } from "@prisma/generated/enums"; import { Type } from "class-transformer"; import { IsEnum, IsInt, Max, Min } from "class-validator"; +import type { ClientIpType } from "@/shared/decorators/client-ip.decorator"; export class CreatePaymentDto { @IsEnum(PaymentFrequency) diff --git a/src/modules/payment/payment.module.ts b/src/modules/payment/payment.module.ts index 839d7a7..d810dac 100644 --- a/src/modules/payment/payment.module.ts +++ b/src/modules/payment/payment.module.ts @@ -1,11 +1,10 @@ import { Module } from "@nestjs/common"; - -import { StripeController } from "./controller/stripe.controller"; import { PaymentController } from "./controller/payment.controller"; -import { PaymentService } from "./service/payment.service"; -import { StripeService } from "./service/stripe.service"; import { PerkController } from "./controller/perk.controller"; +import { StripeController } from "./controller/stripe.controller"; +import { PaymentService } from "./service/payment.service"; import { PerkService } from "./service/perk.service"; +import { StripeService } from "./service/stripe.service"; @Module({ imports: [], diff --git a/src/modules/payment/service/payment.service.ts b/src/modules/payment/service/payment.service.ts index 74168a6..dc29bfe 100644 --- a/src/modules/payment/service/payment.service.ts +++ b/src/modules/payment/service/payment.service.ts @@ -1,15 +1,15 @@ import { Injectable } from "@nestjs/common"; -import { StripeService } from "./stripe.service"; import { ConfigService } from "@nestjs/config"; -import { DatabaseService } from "@/shared/infra/database/database.service"; -import { AppException } from "@/shared/exceptions/app.exceptions"; -import { ERROR_CODES } from "@/shared/constants/error-codes"; import { PaymentFrequency, PaymentStatus } from "@prisma/generated/enums"; -import { GetPaymentsDto } from "../dto/get-payments.dto"; import { PaymentFindManyArgs } from "@prisma/generated/models"; -import { CreatePaymentDto } from "../dto/create-payment.dto"; -import { getUserCurrency } from "@/shared/utils/currency"; +import { ERROR_CODES } from "@/shared/constants/error-codes"; import { DEFAULT_CURRENCY } from "@/shared/constants/payment"; +import { AppException } from "@/shared/exceptions/app.exceptions"; +import { DatabaseService } from "@/shared/infra/database/database.service"; +import { getUserCurrency } from "@/shared/utils/currency"; +import { CreatePaymentDto } from "../dto/create-payment.dto"; +import { GetPaymentsDto } from "../dto/get-payments.dto"; +import { StripeService } from "./stripe.service"; @Injectable() export class PaymentService { diff --git a/src/modules/payment/service/perk.service.ts b/src/modules/payment/service/perk.service.ts index 320bcbb..bf42f7c 100644 --- a/src/modules/payment/service/perk.service.ts +++ b/src/modules/payment/service/perk.service.ts @@ -1,7 +1,7 @@ import { forwardRef, Inject, Injectable } from "@nestjs/common"; -import { StripeService } from "./stripe.service"; import { UserTier } from "@prisma/generated/enums"; import type { ClientIpType } from "@/shared/decorators/client-ip.decorator"; +import { StripeService } from "./stripe.service"; @Injectable() export class PerkService { diff --git a/src/modules/payment/service/stripe.service.ts b/src/modules/payment/service/stripe.service.ts index 33aea29..c050690 100644 --- a/src/modules/payment/service/stripe.service.ts +++ b/src/modules/payment/service/stripe.service.ts @@ -1,19 +1,19 @@ -import { DatabaseService } from "@/shared/infra/database/database.service"; -import { formatValue, getUserCurrency } from "@/shared/utils/currency"; +import { HttpService } from "@nestjs/axios"; import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { PaymentStatus } from "@prisma/generated/enums"; +import { firstValueFrom } from "rxjs"; import Stripe from "stripe"; -import { QueueService } from "@/shared/infra/queue/queue.service"; -import { DEFAULT_CURRENCY } from "@/shared/constants/payment"; -import { AppException } from "@/shared/exceptions/app.exceptions"; +import { CACHE_KEYS } from "@/shared/constants/cache"; import { ERROR_CODES } from "@/shared/constants/error-codes"; +import { DEFAULT_CURRENCY } from "@/shared/constants/payment"; import type { ClientIpType } from "@/shared/decorators/client-ip.decorator"; -import { PerkService } from "./perk.service"; -import { HttpService } from "@nestjs/axios"; +import { AppException } from "@/shared/exceptions/app.exceptions"; import { CacheService } from "@/shared/infra/cache/cache.service"; -import { CACHE_KEYS } from "@/shared/constants/cache"; -import { firstValueFrom } from "rxjs"; +import { DatabaseService } from "@/shared/infra/database/database.service"; +import { QueueService } from "@/shared/infra/queue/queue.service"; +import { formatValue, getUserCurrency } from "@/shared/utils/currency"; +import { PerkService } from "./perk.service"; export interface PriceValue { raw: number; diff --git a/src/modules/profile/controller/profile.controller.ts b/src/modules/profile/controller/profile.controller.ts index f78cfcf..d5841ba 100644 --- a/src/modules/profile/controller/profile.controller.ts +++ b/src/modules/profile/controller/profile.controller.ts @@ -1,10 +1,10 @@ import { Body, Controller, Delete, Patch, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common"; import { FileInterceptor } from "@nestjs/platform-express"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; +import { imageConfig } from "@/shared/infra/upload/upload.config"; import { UpdateProfileDto } from "../dto/update-profile.dto"; import { ProfileService } from "../service/profile.service"; -import { imageConfig } from "@/shared/infra/upload/upload.config"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("Profile") @Controller("/profile") diff --git a/src/modules/reaction/controller/reaction.controller.ts b/src/modules/reaction/controller/reaction.controller.ts index a910245..11cde5a 100644 --- a/src/modules/reaction/controller/reaction.controller.ts +++ b/src/modules/reaction/controller/reaction.controller.ts @@ -11,11 +11,11 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { CreateReactionDto } from "../dto/create-reaction.dto"; import { GetReactionsDto } from "../dto/get-reactions.dto"; import { ReactionService } from "../service/reaction.service"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("Reaction") @Controller("/reaction") diff --git a/src/modules/reaction/dto/create-reaction.dto.ts b/src/modules/reaction/dto/create-reaction.dto.ts index 0ec3a5f..c9a73d7 100644 --- a/src/modules/reaction/dto/create-reaction.dto.ts +++ b/src/modules/reaction/dto/create-reaction.dto.ts @@ -59,4 +59,34 @@ export class CreateReactionDto { description: "Required when type is FeedEvent", }) readonly feedEventId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.GameReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is GameReview" }) + readonly gameReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.AnimeReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is AnimeReview" }) + readonly animeReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.MangaReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is MangaReview" }) + readonly mangaReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.TvShowReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is TvShowReview" }) + readonly tvShowReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.MovieReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is MovieReview" }) + readonly movieReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.BookReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is BookReview" }) + readonly bookReviewId?: string; } diff --git a/src/modules/reaction/dto/get-reactions.dto.ts b/src/modules/reaction/dto/get-reactions.dto.ts index 989710d..005fa2f 100644 --- a/src/modules/reaction/dto/get-reactions.dto.ts +++ b/src/modules/reaction/dto/get-reactions.dto.ts @@ -28,4 +28,34 @@ export class GetReactionsDto extends OffsetPaginationParamsDto { description: "Required when type is FeedEvent", }) readonly feedEventId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.GameReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is GameReview" }) + readonly gameReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.AnimeReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is AnimeReview" }) + readonly animeReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.MangaReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is MangaReview" }) + readonly mangaReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.TvShowReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is TvShowReview" }) + readonly tvShowReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.MovieReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is MovieReview" }) + readonly movieReviewId?: string; + + @IsOptional() + @ReactionRequiredForType(ReactionType.BookReview) + @ApiProperty({ type: "string", format: "uuid", required: false, description: "Required when type is BookReview" }) + readonly bookReviewId?: string; } diff --git a/src/modules/reaction/service/reaction.service.ts b/src/modules/reaction/service/reaction.service.ts index 63f13ff..cc9fe3c 100644 --- a/src/modules/reaction/service/reaction.service.ts +++ b/src/modules/reaction/service/reaction.service.ts @@ -45,6 +45,12 @@ export class ReactionService { type: getReactionsDto.type, commentId: getReactionsDto.commentId, feedEventId: getReactionsDto.feedEventId, + gameReviewId: getReactionsDto.gameReviewId, + animeReviewId: getReactionsDto.animeReviewId, + mangaReviewId: getReactionsDto.mangaReviewId, + tvShowReviewId: getReactionsDto.tvShowReviewId, + movieReviewId: getReactionsDto.movieReviewId, + bookReviewId: getReactionsDto.bookReviewId, }, page: getReactionsDto.page, itemsPerPage: getReactionsDto.itemsPerPage, diff --git a/src/modules/tv-show/controller/tv-show-episode-watch.controller.ts b/src/modules/tv-show/controller/tv-show-episode-watch.controller.ts index 93990d2..0a7347f 100644 --- a/src/modules/tv-show/controller/tv-show-episode-watch.controller.ts +++ b/src/modules/tv-show/controller/tv-show-episode-watch.controller.ts @@ -1,12 +1,12 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { CreateOrUpdateTVShowEpisodeWatchDto } from "../dto/create-or-update-tv-show-episode-watch.dto"; -import { DeleteTVShowEpisodeWatchDto } from "../dto/delete-tv-show-episode-watch.dto"; import { DeleteAllTVShowEpisodeWatchDto } from "../dto/delete-all-tv-show-episode-watch.dto"; -import { TVShowEpisodeWatchService } from "../service/tv-show-episode-watch.service"; +import { DeleteTVShowEpisodeWatchDto } from "../dto/delete-tv-show-episode-watch.dto"; import { GetTVShowEpisodeWatchDto } from "../dto/get-tv-show-episode-watch.dto"; import { WatchAllEpisodesOfTVShowDto } from "../dto/watch-all-episodes-of-tv-show.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { TVShowEpisodeWatchService } from "../service/tv-show-episode-watch.service"; @ApiTags("TV Show") @Controller("/tv/episode/watch") diff --git a/src/modules/tv-show/controller/tv-show-progress.controller.ts b/src/modules/tv-show/controller/tv-show-progress.controller.ts index bf4d93c..c78b1be 100644 --- a/src/modules/tv-show/controller/tv-show-progress.controller.ts +++ b/src/modules/tv-show/controller/tv-show-progress.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { TVShowProgressService } from "../service/tv-show-progress.service"; import { CreateOrUpdateTVShowProgressDto } from "../dto/create-or-update-tv-show-progress.dto"; import { GetTVShowProgressDto } from "../dto/get-tv-show-progress.dto"; -import { ApiTags } from "@nestjs/swagger"; +import { TVShowProgressService } from "../service/tv-show-progress.service"; @ApiTags("TV Show") @Controller("/tv/progress") diff --git a/src/modules/tv-show/controller/tv-show-review.controller.ts b/src/modules/tv-show/controller/tv-show-review.controller.ts index e01327a..8134406 100644 --- a/src/modules/tv-show/controller/tv-show-review.controller.ts +++ b/src/modules/tv-show/controller/tv-show-review.controller.ts @@ -12,12 +12,12 @@ import { Query, UseGuards, } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { CreateTVShowReviewDto } from "../dto/create-tv-show-review.dto"; import { GetTVShowReviewsDto } from "../dto/get-tv-show-reviews.dto"; import { UpdateTVShowReviewDto } from "../dto/update-tv-show-review.dto"; import { TVShowReviewService } from "../service/tv-show-review.service"; -import { ApiTags } from "@nestjs/swagger"; @ApiTags("TV Show") @Controller("/tv/review") diff --git a/src/modules/tv-show/controller/tv-show.controller.ts b/src/modules/tv-show/controller/tv-show.controller.ts index bd49c31..060a95a 100644 --- a/src/modules/tv-show/controller/tv-show.controller.ts +++ b/src/modules/tv-show/controller/tv-show.controller.ts @@ -1,6 +1,19 @@ -import { Body, Controller, Get, Param, ParseIntPipe, Post, Query, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseIntPipe, + ParseUUIDPipe, + Post, + Query, + UseGuards, +} from "@nestjs/common"; import { ApiTags } from "@nestjs/swagger"; -import { AuthGuard } from "@thallesp/nestjs-better-auth"; +import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { TopTvShowDto } from "@/modules/tv-show/dto/top-tv-show.dto"; import { RefreshTVShowDto } from "../dto/refresh-tv-show.dto"; import { SearchTVShowDto } from "../dto/search-tv-show.dto"; @@ -61,4 +74,11 @@ export class TVShowController { return { episodes }; } + + @Delete("/tracking/:tvShowId") + @HttpCode(HttpStatus.NO_CONTENT) + @UseGuards(AuthGuard) + async resetTVShowTracking(@Param("tvShowId", new ParseUUIDPipe()) tvShowId: string, @Session() session: UserSession) { + await this.tvShowService.resetTVShowTracking({ tvShowId, userId: session.user.id }); + } } diff --git a/src/modules/tv-show/dto/create-or-update-tv-show-progress.dto.ts b/src/modules/tv-show/dto/create-or-update-tv-show-progress.dto.ts index c0ec7f2..fb4c47b 100644 --- a/src/modules/tv-show/dto/create-or-update-tv-show-progress.dto.ts +++ b/src/modules/tv-show/dto/create-or-update-tv-show-progress.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ProgressStatus } from "@prisma/generated/enums"; -import { Transform } from "class-transformer"; -import { IsDate, IsEnum, IsInt, IsNotEmpty, IsOptional, IsPositive } from "class-validator"; +import { Transform, Type } from "class-transformer"; +import { IsDate, IsEnum, IsInt, IsNotEmpty, IsOptional, IsPositive, MaxLength } from "class-validator"; export class CreateOrUpdateTVShowProgressDto { @IsEnum(ProgressStatus) @@ -20,15 +20,22 @@ export class CreateOrUpdateTVShowProgressDto { readonly watchCount?: number; @IsOptional() + @Type(() => Date) @IsDate() @ApiPropertyOptional({ type: "string", format: "date-time", example: "2026-03-12T06:20:32.232Z" }) readonly startedAt?: Date; @IsOptional() + @Type(() => Date) @IsDate() @ApiPropertyOptional({ type: "string", format: "date-time", example: "2026-03-12T06:20:32.232Z" }) readonly completedAt?: Date; + @IsOptional() + @MaxLength(1000) + @ApiPropertyOptional({ type: "string", maxLength: 1000 }) + readonly notes?: string; + @IsNotEmpty() @ApiProperty({ description: "ID of the TV show", diff --git a/src/modules/tv-show/dto/get-tv-show-reviews.dto.ts b/src/modules/tv-show/dto/get-tv-show-reviews.dto.ts index ded6e26..90eecc3 100644 --- a/src/modules/tv-show/dto/get-tv-show-reviews.dto.ts +++ b/src/modules/tv-show/dto/get-tv-show-reviews.dto.ts @@ -1,5 +1,5 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional } from "class-validator"; +import { IsOptional, IsString } from "class-validator"; import { OffsetPaginationParamsDto } from "@/shared/infra/database/dtos/offset-pagination.dto"; export class GetTVShowReviewsDto extends OffsetPaginationParamsDto { @@ -18,4 +18,13 @@ export class GetTVShowReviewsDto extends OffsetPaginationParamsDto { type: "string", }) readonly userId?: string; + + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: "Search query to filter reviews by media title", + example: "breaking bad", + type: "string", + }) + readonly query?: string; } diff --git a/src/modules/tv-show/dto/reset-tv-show-tracking.dto.ts b/src/modules/tv-show/dto/reset-tv-show-tracking.dto.ts new file mode 100644 index 0000000..ec7127f --- /dev/null +++ b/src/modules/tv-show/dto/reset-tv-show-tracking.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsUUID } from "class-validator"; + +export class ResetTVShowTrackingDto { + @IsNotEmpty() + @IsUUID() + readonly tvShowId: string; + + readonly userId: string; +} diff --git a/src/modules/tv-show/service/tv-show-episode-watch.service.ts b/src/modules/tv-show/service/tv-show-episode-watch.service.ts index 33f0b51..ed54be1 100644 --- a/src/modules/tv-show/service/tv-show-episode-watch.service.ts +++ b/src/modules/tv-show/service/tv-show-episode-watch.service.ts @@ -1,14 +1,14 @@ import { Injectable } from "@nestjs/common"; -import { CreateOrUpdateTVShowEpisodeWatchDto } from "../dto/create-or-update-tv-show-episode-watch.dto"; +import { WatchEpisodeStatus } from "@prisma/generated/enums"; +import { ERROR_CODES } from "@/shared/constants/error-codes"; +import { AppException } from "@/shared/exceptions/app.exceptions"; import { DatabaseService } from "@/shared/infra/database/database.service"; +import { TMDBTVShowSeason } from "@/shared/infra/integrations/tmdb.service"; +import { CreateOrUpdateTVShowEpisodeWatchDto } from "../dto/create-or-update-tv-show-episode-watch.dto"; +import { DeleteAllTVShowEpisodeWatchDto } from "../dto/delete-all-tv-show-episode-watch.dto"; +import { DeleteTVShowEpisodeWatchDto } from "../dto/delete-tv-show-episode-watch.dto"; import { GetTVShowEpisodeWatchDto } from "../dto/get-tv-show-episode-watch.dto"; import { WatchAllEpisodesOfTVShowDto } from "../dto/watch-all-episodes-of-tv-show.dto"; -import { DeleteTVShowEpisodeWatchDto } from "../dto/delete-tv-show-episode-watch.dto"; -import { DeleteAllTVShowEpisodeWatchDto } from "../dto/delete-all-tv-show-episode-watch.dto"; -import { AppException } from "@/shared/exceptions/app.exceptions"; -import { ERROR_CODES } from "@/shared/constants/error-codes"; -import { WatchEpisodeStatus } from "@prisma/generated/enums"; -import { TMDBTVShowSeason } from "@/shared/infra/integrations/tmdb.service"; @Injectable() export class TVShowEpisodeWatchService { @@ -140,10 +140,7 @@ export class TVShowEpisodeWatchService { userId: getTVShowEpisodeWatchDto.userId, tvShowId: getTVShowEpisodeWatchDto.tvShowId, }, - orderBy: { - season: "asc", - episode: "asc", - }, + orderBy: [{ season: "asc" }, { episode: "asc" }], }); return tvShowEpisodeWatch; diff --git a/src/modules/tv-show/service/tv-show-progress.service.ts b/src/modules/tv-show/service/tv-show-progress.service.ts index e7c4a78..35e9506 100644 --- a/src/modules/tv-show/service/tv-show-progress.service.ts +++ b/src/modules/tv-show/service/tv-show-progress.service.ts @@ -1,13 +1,13 @@ -import { DatabaseService } from "@/shared/infra/database/database.service"; import { Injectable } from "@nestjs/common"; -import { CreateOrUpdateTVShowProgressDto } from "../dto/create-or-update-tv-show-progress.dto"; -import { GetTVShowProgressDto } from "../dto/get-tv-show-progress.dto"; +import { FeedEventType, ProgressStatus } from "@prisma/generated/enums"; import { TvShowProgressFindManyArgs } from "@prisma/generated/models"; -import { AppException } from "@/shared/exceptions/app.exceptions"; import { ERROR_CODES } from "@/shared/constants/error-codes"; -import { TVShowEpisodeWatchService } from "./tv-show-episode-watch.service"; -import { FeedEventType, ProgressStatus } from "@prisma/generated/enums"; +import { AppException } from "@/shared/exceptions/app.exceptions"; +import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; +import { CreateOrUpdateTVShowProgressDto } from "../dto/create-or-update-tv-show-progress.dto"; +import { GetTVShowProgressDto } from "../dto/get-tv-show-progress.dto"; +import { TVShowEpisodeWatchService } from "./tv-show-episode-watch.service"; @Injectable() export class TVShowProgressService { @@ -18,7 +18,7 @@ export class TVShowProgressService { ) {} async createOrUpdateTVShowProgress(createOrUpdateTVShowProgressDto: CreateOrUpdateTVShowProgressDto) { - const { tvShowId, userId, status, watchCount, completedAt, startedAt } = createOrUpdateTVShowProgressDto; + const { tvShowId, userId, status, watchCount, notes, completedAt, startedAt } = createOrUpdateTVShowProgressDto; const tvShowProgress = await this.databaseService.tvShowProgress.upsert({ where: { @@ -30,6 +30,7 @@ export class TVShowProgressService { update: { status, watchCount, + notes, completedAt, startedAt, }, @@ -38,6 +39,7 @@ export class TVShowProgressService { userId, status, watchCount, + notes, completedAt, startedAt, }, @@ -80,7 +82,7 @@ export class TVShowProgressService { async deleteTVShowProgress(tvShowProgressId: string, userId: string) { const tvShowProgress = await this.databaseService.tvShowProgress.findUnique({ where: { id: tvShowProgressId }, - select: { tvShowId: true, userId: true }, + select: { tvShowId: true, userId: true, tvShow: { select: { tmdbId: true } } }, }); if (!tvShowProgress || tvShowProgress.userId !== userId) { diff --git a/src/modules/tv-show/service/tv-show-review.service.ts b/src/modules/tv-show/service/tv-show-review.service.ts index bb7b8e4..d54d26f 100644 --- a/src/modules/tv-show/service/tv-show-review.service.ts +++ b/src/modules/tv-show/service/tv-show-review.service.ts @@ -106,7 +106,11 @@ export class TVShowReviewService { where: { ...(getTVShowReviewsDto.userId && { userId: getTVShowReviewsDto.userId }), ...(getTVShowReviewsDto.tvShowId && { tvShowId: getTVShowReviewsDto.tvShowId }), + ...(getTVShowReviewsDto.query && { + tvShow: { name: { contains: getTVShowReviewsDto.query, mode: "insensitive" } }, + }), }, + orderBy: { createdAt: "desc" }, include: { tvShow: { select: { @@ -129,6 +133,19 @@ export class TVShowReviewService { }, }, }, + reactions: { + select: { + id: true, + emoji: true, + createdAt: true, + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, }, }); @@ -140,6 +157,11 @@ export class TVShowReviewService { where: { id: updateTVShowReviewDto.tvShowReviewId, }, + include: { + tvShow: { + select: { tmdbId: true }, + }, + }, }); if (!tvShowReview || tvShowReview.userId !== updateTVShowReviewDto.userId) { @@ -166,6 +188,11 @@ export class TVShowReviewService { where: { id: deleteTVShowReviewDto.tvShowReviewId, }, + include: { + tvShow: { + select: { tmdbId: true }, + }, + }, }); if (!tvShowReview || tvShowReview.userId !== deleteTVShowReviewDto.userId) { diff --git a/src/modules/tv-show/service/tv-show.service.ts b/src/modules/tv-show/service/tv-show.service.ts index e374741..cacaaf2 100644 --- a/src/modules/tv-show/service/tv-show.service.ts +++ b/src/modules/tv-show/service/tv-show.service.ts @@ -1,5 +1,5 @@ import { Injectable } from "@nestjs/common"; -import { ProgressStatus, TvShow } from "@prisma/generated/client"; +import { ProgressStatus } from "@prisma/generated/client"; import { TvShowCreateInput, TvShowUpdateInput } from "@prisma/generated/models"; import { TopTvShowDto } from "@/modules/tv-show/dto/top-tv-show.dto"; import { CACHE_KEYS } from "@/shared/constants/cache"; @@ -16,6 +16,7 @@ import { TMDBTVShowSeasonEpisode, } from "@/shared/infra/integrations/tmdb.service"; import { RefreshTVShowDto } from "../dto/refresh-tv-show.dto"; +import { ResetTVShowTrackingDto } from "../dto/reset-tv-show-tracking.dto"; import type { SearchTVShowDto } from "../dto/search-tv-show.dto"; @Injectable() @@ -97,12 +98,6 @@ export class TVShowService { } async getTVShowByTmdbId(tmdbId: number) { - const cachedTVShow = await this.cacheService.get(CACHE_KEYS.TV_SHOW_BY_TMDB_ID.prefix(tmdbId)); - - if (cachedTVShow) { - return cachedTVShow; - } - let tvShow = await this.databaseService.tvShow.findUnique({ where: { tmdbId }, omit: { @@ -152,22 +147,10 @@ export class TVShowService { progressStats, }; - await this.cacheService.set( - CACHE_KEYS.TV_SHOW_BY_TMDB_ID.prefix(tmdbId), - tvShowWithStats, - CACHE_KEYS.TV_SHOW_BY_TMDB_ID.expiration, - ); - return tvShowWithStats; } async getTVShowSeasons(tmdbId: number) { - const cachedSeasons = await this.cacheService.get(CACHE_KEYS.TV_SHOW_SEASONS_BY_TMDB_ID.prefix(tmdbId)); - - if (cachedSeasons) { - return cachedSeasons; - } - const tvShow = await this.databaseService.tvShow.findUnique({ where: { tmdbId }, select: { seasons: true }, @@ -177,18 +160,6 @@ export class TVShowService { throw new AppException(ERROR_CODES.TV_SHOW_NOT_FOUND); } - const existingSeasons = (tvShow.seasons ?? []) as unknown as TMDBTVShowSeason[]; - - if (existingSeasons.length > 0) { - await this.cacheService.set( - CACHE_KEYS.TV_SHOW_SEASONS_BY_TMDB_ID.prefix(tmdbId), - existingSeasons, - CACHE_KEYS.TV_SHOW_SEASONS_BY_TMDB_ID.expiration, - ); - - return existingSeasons; - } - const seasons = await this.integrationsService.tmdb.getTVShowSeasonsById(tmdbId); await this.databaseService.tvShow.update({ @@ -196,24 +167,10 @@ export class TVShowService { data: { seasons } as unknown as TvShowUpdateInput, }); - await this.cacheService.set( - CACHE_KEYS.TV_SHOW_SEASONS_BY_TMDB_ID.prefix(tmdbId), - seasons, - CACHE_KEYS.TV_SHOW_SEASONS_BY_TMDB_ID.expiration, - ); - return seasons; } async getTVShowSeasonEpisodes(tmdbId: number, seasonNumber: number) { - const cachedEpisodes = await this.cacheService.get( - CACHE_KEYS.TMDB_TV_SHOW_SEASON_EPISODES_BY_ID.prefix(tmdbId, seasonNumber), - ); - - if (cachedEpisodes) { - return cachedEpisodes; - } - const tvShow = await this.databaseService.tvShow.findUnique({ where: { tmdbId }, select: { episodes: true }, @@ -227,12 +184,6 @@ export class TVShowService { const seasonEpisodes = allEpisodes.filter((ep) => ep.seasonNumber === seasonNumber); if (seasonEpisodes.length > 0) { - await this.cacheService.set( - CACHE_KEYS.TMDB_TV_SHOW_SEASON_EPISODES_BY_ID.prefix(tmdbId, seasonNumber), - seasonEpisodes, - CACHE_KEYS.TMDB_TV_SHOW_SEASON_EPISODES_BY_ID.expiration, - ); - return seasonEpisodes; } @@ -271,14 +222,6 @@ export class TVShowService { throw new AppException(ERROR_CODES.TV_SHOW_ALREADY_REFRESHED); } - if (await this.cacheService.exists(CACHE_KEYS.TV_SHOW_BY_TMDB_ID.prefix(refreshTVShowDto.tmdbId))) { - await this.cacheService.delete(CACHE_KEYS.TV_SHOW_BY_TMDB_ID.prefix(refreshTVShowDto.tmdbId)); - } - - if (await this.cacheService.exists(CACHE_KEYS.TV_SHOW_SEASONS_BY_TMDB_ID.prefix(refreshTVShowDto.tmdbId))) { - await this.cacheService.delete(CACHE_KEYS.TV_SHOW_SEASONS_BY_TMDB_ID.prefix(refreshTVShowDto.tmdbId)); - } - const existingSeasons = (tvShow.seasons ?? []) as unknown as TMDBTVShowSeason[]; for (const season of existingSeasons) { @@ -323,4 +266,12 @@ export class TVShowService { await this.getTVShowByTmdbId(refreshTVShowDto.tmdbId); } + + async resetTVShowTracking({ userId, tvShowId }: ResetTVShowTrackingDto) { + await this.databaseService.$transaction([ + this.databaseService.tvShowReview.deleteMany({ where: { userId, tvShowId } }), + this.databaseService.tvShowProgress.deleteMany({ where: { userId, tvShowId } }), + this.databaseService.tvShowEpisodeWatch.deleteMany({ where: { userId, tvShowId } }), + ]); + } } diff --git a/src/modules/tv-show/tv-show.module.ts b/src/modules/tv-show/tv-show.module.ts index 52408b7..750fcf3 100644 --- a/src/modules/tv-show/tv-show.module.ts +++ b/src/modules/tv-show/tv-show.module.ts @@ -1,13 +1,13 @@ import { Module } from "@nestjs/common"; import { TVShowController } from "./controller/tv-show.controller"; -import { TVShowService } from "./service/tv-show.service"; -import { TVShowReviewController } from "./controller/tv-show-review.controller"; -import { TVShowReviewService } from "./service/tv-show-review.service"; -import { TVShowProgressController } from "./controller/tv-show-progress.controller"; -import { TVShowProgressService } from "./service/tv-show-progress.service"; import { TVShowEpisodeWatchController } from "./controller/tv-show-episode-watch.controller"; +import { TVShowProgressController } from "./controller/tv-show-progress.controller"; +import { TVShowReviewController } from "./controller/tv-show-review.controller"; +import { TVShowService } from "./service/tv-show.service"; import { TVShowEpisodeWatchService } from "./service/tv-show-episode-watch.service"; +import { TVShowProgressService } from "./service/tv-show-progress.service"; +import { TVShowReviewService } from "./service/tv-show-review.service"; @Module({ imports: [], diff --git a/src/modules/user/controller/user.controller.ts b/src/modules/user/controller/user.controller.ts index ed36760..c904050 100644 --- a/src/modules/user/controller/user.controller.ts +++ b/src/modules/user/controller/user.controller.ts @@ -1,11 +1,11 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query, UseGuards } from "@nestjs/common"; -import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; -import { UserService } from "../service/user.service"; import { ApiTags } from "@nestjs/swagger"; +import { AuthGuard, Session, type UserSession } from "@thallesp/nestjs-better-auth"; import { GetFollowersDto } from "../dto/get-followers.dto"; import { GetFollowingDto } from "../dto/get-following.dto"; import { SearchUserDto } from "../dto/search-user.dto"; import { UpdateUserDto } from "../dto/update-user.dto"; +import { UserService } from "../service/user.service"; @ApiTags("User") @Controller("/user") @@ -54,6 +54,14 @@ export class UserController { await this.userService.unfollowUser(session.user.id, unfollowId); } + @Get("/follow-status/:username") + @UseGuards(AuthGuard) + async getFollowStatus(@Session() session: UserSession, @Param("username") username: string) { + const followStatus = await this.userService.getFollowStatus(session.user.id, username); + + return { followStatus }; + } + @Get("/follower") async getFollowers(@Query() getFollowersDto: GetFollowersDto) { const followers = await this.userService.getFollowers(getFollowersDto); diff --git a/src/modules/user/service/user.service.ts b/src/modules/user/service/user.service.ts index 4b859f2..c90e428 100644 --- a/src/modules/user/service/user.service.ts +++ b/src/modules/user/service/user.service.ts @@ -1,12 +1,12 @@ import { Injectable } from "@nestjs/common"; import { FeedEventType, ProgressStatus } from "@prisma/generated/enums"; +import { FollowingFindManyArgs, UserFindManyArgs } from "@prisma/generated/models"; import { ERROR_CODES } from "@/shared/constants/error-codes"; import { AppException } from "@/shared/exceptions/app.exceptions"; import { DatabaseService } from "@/shared/infra/database/database.service"; import { QueueService } from "@/shared/infra/queue/queue.service"; import { extractNameFromEmail } from "@/shared/utils/email"; import { GetFollowersDto } from "../dto/get-followers.dto"; -import { FollowingFindManyArgs, UserFindManyArgs } from "@prisma/generated/models"; import { SearchUserDto } from "../dto/search-user.dto"; import { UpdateUserDto } from "../dto/update-user.dto"; @@ -97,6 +97,19 @@ export class UserService { select: { followers: true, following: true, + lists: true, + favorites: true, + animeReviews: true, + mangaReviews: true, + tvshowReviews: true, + movieReviews: true, + gameReviews: true, + bookReviews: true, + }, + }, + userMedals: { + select: { + medal: true, }, }, }, @@ -189,7 +202,50 @@ export class UserService { book: buildStats(bookGroups, readingStatuses), }; - return { ...user, progressStats }; + const counts = { + lists: user._count.lists, + favorites: user._count.favorites, + reviews: + user._count.animeReviews + + user._count.mangaReviews + + user._count.tvshowReviews + + user._count.movieReviews + + user._count.gameReviews + + user._count.bookReviews, + }; + + const reviewModels = [ + { type: "movie", model: this.databaseService.movieReview }, + { type: "tv", model: this.databaseService.tvShowReview }, + { type: "anime", model: this.databaseService.animeReview }, + { type: "game", model: this.databaseService.gameReview }, + { type: "book", model: this.databaseService.bookReview }, + { type: "manga", model: this.databaseService.mangaReview }, + ] as const; + + const latestReviews = await Promise.all( + reviewModels.map(({ model }) => + (model as { findFirst: (args: unknown) => Promise<{ createdAt: Date } | null> }).findFirst({ + where: { userId: user.id }, + orderBy: { createdAt: "desc" }, + select: { createdAt: true }, + }), + ), + ); + + let latestReviewType: string | null = null; + let latestReviewDate = 0; + + reviewModels.forEach(({ type }, index) => { + const createdAt = latestReviews[index]?.createdAt; + + if (createdAt && createdAt.getTime() > latestReviewDate) { + latestReviewDate = createdAt.getTime(); + latestReviewType = type; + } + }); + + return { ...user, progressStats, counts, latestReviewType }; } getName(name: string | null | undefined, email: string) { @@ -298,6 +354,41 @@ export class UserService { }); } + async getFollowStatus(currentUserId: string, username: string) { + const targetUser = await this.databaseService.user.findUnique({ + where: { username }, + select: { id: true }, + }); + + if (!targetUser) { + throw new AppException(ERROR_CODES.USER_NOT_FOUND); + } + + const [isFollowing, followsYou] = await Promise.all([ + this.databaseService.following.findUnique({ + where: { + followerId_followingId: { + followerId: currentUserId, + followingId: targetUser.id, + }, + }, + }), + this.databaseService.following.findUnique({ + where: { + followerId_followingId: { + followerId: targetUser.id, + followingId: currentUserId, + }, + }, + }), + ]); + + return { + isFollowing: Boolean(isFollowing), + followsYou: Boolean(followsYou), + }; + } + async getFollowers(getFollowersDto: GetFollowersDto) { const followers = await this.databaseService.offsetPagination({ model: "following", diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 9634043..ee687db 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -1,7 +1,6 @@ import { Global, Module } from "@nestjs/common"; - -import { UserService } from "./service/user.service"; import { UserController } from "./controller/user.controller"; +import { UserService } from "./service/user.service"; @Global() @Module({ diff --git a/src/shared/constants/cache.ts b/src/shared/constants/cache.ts index bb5fc8e..62023be 100644 --- a/src/shared/constants/cache.ts +++ b/src/shared/constants/cache.ts @@ -9,53 +9,6 @@ function filtersToString(filters: Record) { } export const CACHE_KEYS = { - ANIME_BY_MAL_ID: { - prefix: (malId: number) => `anime:detail:malId:${malId}`, - expiration: 3600 * 24, - }, - ANIME_RELATIONS_BY_MAL_ID: { - prefix: (malId: number) => `anime:detail:malId:${malId}:relation`, - expiration: 3600 * 24, - }, - ANIME_EPISODES_BY_MAL_ID: { - prefix: ({ malId, ...filters }: Record) => - `anime:detail:malId:${malId}:episode:${filtersToString(filters)}`, - expiration: 3600 * 24, - }, - - BOOK_BY_HARDCOVER_ID: { - prefix: (hardcoverId: number) => `book:detail:hardcoverId:${hardcoverId}`, - expiration: 3600 * 24, - }, - - GAME_BY_IGDB_ID: { - prefix: (igdbId: number) => `game:detail:igdbId:${igdbId}`, - expiration: 3600 * 24, - }, - - MANGA_BY_MAL_ID: { - prefix: (malId: number) => `manga:detail:malId:${malId}`, - expiration: 3600 * 24, - }, - MANGA_RELATIONS_BY_MAL_ID: { - prefix: (malId: number) => `manga:detail:malId:${malId}:relation`, - expiration: 3600 * 24, - }, - - MOVIE_BY_IMDB_ID: { - prefix: (imdbId: number) => `movie:detail:imdbId:${imdbId}`, - expiration: 3600 * 24, - }, - - TV_SHOW_BY_TMDB_ID: { - prefix: (tmdbId: number) => `tvShow:detail:tmdbId:${tmdbId}`, - expiration: 3600 * 24, - }, - TV_SHOW_SEASONS_BY_TMDB_ID: { - prefix: (tmdbId: number) => `tvShow:detail:tmdbId:${tmdbId}:season`, - expiration: 3600 * 24, - }, - HARDCOVER_SEARCH_BOOKS: { prefix: (filters: Record) => `hardcover:search:book:${filtersToString(filters)}`, expiration: 3600 * 24 * 7, diff --git a/src/shared/infra/email/email.service.ts b/src/shared/infra/email/email.service.ts index 39c0470..6740032 100644 --- a/src/shared/infra/email/email.service.ts +++ b/src/shared/infra/email/email.service.ts @@ -5,10 +5,10 @@ import { ConfigService } from "@nestjs/config"; import handlebars from "handlebars"; import { ResendService } from "nestjs-resend"; import { MagicLinkEmailDto } from "./dto/magic-link-email.dto"; -import { ResetPasswordEmailDto } from "./dto/reset-password-email.dto"; +import { PaymentFailedEmailDto } from "./dto/payment-failed-email.dto"; import { PaymentSuccessEmailDto } from "./dto/payment-success-email.dto"; +import { ResetPasswordEmailDto } from "./dto/reset-password-email.dto"; import { SubscriptionCancelledEmailDto } from "./dto/subscription-cancelled-email.dto"; -import { PaymentFailedEmailDto } from "./dto/payment-failed-email.dto"; @Injectable() export class EmailService { diff --git a/src/shared/infra/integrations/igdb.service.ts b/src/shared/infra/integrations/igdb.service.ts index 1e2ea0f..3a8c2b0 100644 --- a/src/shared/infra/integrations/igdb.service.ts +++ b/src/shared/infra/integrations/igdb.service.ts @@ -345,7 +345,7 @@ export class IGDBService { status ? (status === "Not Released" ? "game_status = null" : `game_status.status = "${status}"`) : null, ].filter(Boolean); - const whereClause = whereConditions.length ? `where ${whereConditions.join(" & ")}` : ""; + const whereClause = whereConditions.length ? `where ${whereConditions.join(" & ")};` : ""; const igdbSearchQuery = ` fields @@ -367,7 +367,7 @@ export class IGDBService { game_modes.slug, total_rating, first_release_date; - ${whereClause}; + ${whereClause} sort ${orderBy} ${sort}; limit ${limit}; offset ${offset}; @@ -666,7 +666,7 @@ export class IGDBService { const queries: Record = { popular: ` - fields + fields slug, name, cover.url, artworks.checksum, artworks.artwork_type.name, artworks.url, platforms.checksum, platforms.name, platforms.slug, @@ -683,7 +683,7 @@ export class IGDBService { offset ${offset}; `, coming: ` - fields + fields slug, name, cover.url, artworks.checksum, artworks.artwork_type.name, artworks.url, platforms.checksum, platforms.name, platforms.slug, @@ -698,7 +698,7 @@ export class IGDBService { offset ${offset}; `, antecipated: ` - fields + fields slug, name, cover.url, artworks.checksum, artworks.artwork_type.name, artworks.url, platforms.checksum, platforms.name, platforms.slug, @@ -713,7 +713,7 @@ export class IGDBService { offset ${offset}; `, recentlyReleased: ` - fields + fields slug, name, cover.url, artworks.checksum, artworks.artwork_type.name, artworks.url, platforms.checksum, platforms.name, platforms.slug, @@ -803,7 +803,7 @@ export class IGDBService { } const igdbQuery = ` - fields + fields age_ratings.checksum, age_ratings.rating_category.rating, age_ratings.rating_cover_url, diff --git a/src/shared/infra/integrations/tmdb.service.ts b/src/shared/infra/integrations/tmdb.service.ts index a10bfa7..e45a689 100644 --- a/src/shared/infra/integrations/tmdb.service.ts +++ b/src/shared/infra/integrations/tmdb.service.ts @@ -270,7 +270,7 @@ export class TMDBService { ) {} async searchTVShows({ - query, + query = "A", page = DEFAULT_PAGINATION_PAGE, orderBy, sort = TMDBSort.Desc, @@ -360,7 +360,7 @@ export class TMDBService { } async searchMovies({ - query, + query = "A", page = DEFAULT_PAGINATION_PAGE, orderBy, sort = TMDBSort.Desc, diff --git a/src/shared/infra/metrics/metrics.module.ts b/src/shared/infra/metrics/metrics.module.ts index 0eb3a93..70b29f7 100644 --- a/src/shared/infra/metrics/metrics.module.ts +++ b/src/shared/infra/metrics/metrics.module.ts @@ -1,6 +1,5 @@ import { Module } from "@nestjs/common"; -import { makeCounterProvider, makeHistogramProvider } from "@willsoto/nestjs-prometheus"; -import { PrometheusModule } from "@willsoto/nestjs-prometheus"; +import { makeCounterProvider, makeHistogramProvider, PrometheusModule } from "@willsoto/nestjs-prometheus"; @Module({ imports: [ diff --git a/src/shared/infra/queue/processors/email.processor.ts b/src/shared/infra/queue/processors/email.processor.ts index 42819e5..82abce2 100644 --- a/src/shared/infra/queue/processors/email.processor.ts +++ b/src/shared/infra/queue/processors/email.processor.ts @@ -1,9 +1,6 @@ import { OnWorkerEvent, Processor, WorkerHost } from "@nestjs/bullmq"; import { Logger } from "@nestjs/common"; import { Job } from "bullmq"; - -import { EmailService } from "@/shared/infra/email/email.service"; -import { EMAIL_QUEUE } from "@/shared/constants/queue"; import { MAGIC_LINK_JOB, PAYMENT_FAILED_JOB, @@ -11,6 +8,8 @@ import { RESET_PASSWORD_JOB, SUBSCRIPTION_CANCELLED_JOB, } from "@/shared/constants/job"; +import { EMAIL_QUEUE } from "@/shared/constants/queue"; +import { EmailService } from "@/shared/infra/email/email.service"; @Processor(EMAIL_QUEUE, { concurrency: 10 }) export class EmailProcessor extends WorkerHost { diff --git a/src/shared/infra/queue/processors/feed-event.processor.ts b/src/shared/infra/queue/processors/feed-event.processor.ts index 8bc2046..3bb9ce6 100644 --- a/src/shared/infra/queue/processors/feed-event.processor.ts +++ b/src/shared/infra/queue/processors/feed-event.processor.ts @@ -1,13 +1,12 @@ import { OnWorkerEvent, Processor, WorkerHost } from "@nestjs/bullmq"; import { Logger } from "@nestjs/common"; import { Job } from "bullmq"; - +import { FeedEventDto, FeedEventMetadata } from "@/modules/feed-event/dto/feed-event.dto"; import { FeedEventService } from "@/modules/feed-event/service/feed-event.service"; -import { FEED_EVENT_QUEUE } from "@/shared/constants/queue"; import { FEED_EVENT_FLUSH_AGGREGATION_JOB, FEED_EVENT_JOB } from "@/shared/constants/job"; -import { FeedEventDto, FeedEventMetadata } from "@/modules/feed-event/dto/feed-event.dto"; -import { QueueService } from "../queue.service"; +import { FEED_EVENT_QUEUE } from "@/shared/constants/queue"; import { CacheService } from "../../cache/cache.service"; +import { QueueService } from "../queue.service"; export type FeedEventJobData = FeedEventDto; diff --git a/src/shared/infra/queue/queue.module.ts b/src/shared/infra/queue/queue.module.ts index 4faad00..41c8c92 100644 --- a/src/shared/infra/queue/queue.module.ts +++ b/src/shared/infra/queue/queue.module.ts @@ -2,11 +2,11 @@ import { BullModule } from "@nestjs/bullmq"; import { Global, Module } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { FeedEventModule } from "@/modules/feed-event/feed-event.module"; +import { EMAIL_QUEUE, FEED_EVENT_QUEUE } from "@/shared/constants/queue"; import { EmailModule } from "../email/email.module"; import { EmailProcessor } from "./processors/email.processor"; import { FeedEventProcessor } from "./processors/feed-event.processor"; import { QueueService } from "./queue.service"; -import { EMAIL_QUEUE, FEED_EVENT_QUEUE } from "@/shared/constants/queue"; @Global() @Module({ diff --git a/src/shared/infra/queue/queue.service.ts b/src/shared/infra/queue/queue.service.ts index 7222fa6..9727a6a 100644 --- a/src/shared/infra/queue/queue.service.ts +++ b/src/shared/infra/queue/queue.service.ts @@ -2,20 +2,20 @@ import { InjectQueue } from "@nestjs/bullmq"; import { Injectable, Logger } from "@nestjs/common"; import { JobsOptions, Queue } from "bullmq"; import { FeedEventDto } from "@/modules/feed-event/dto/feed-event.dto"; -import { MagicLinkEmailDto } from "../email/dto/magic-link-email.dto"; -import { ResetPasswordEmailDto } from "../email/dto/reset-password-email.dto"; -import { EMAIL_QUEUE, FEED_EVENT_QUEUE } from "@/shared/constants/queue"; import { + FEED_EVENT_FLUSH_AGGREGATION_JOB, FEED_EVENT_JOB, MAGIC_LINK_JOB, - RESET_PASSWORD_JOB, - FEED_EVENT_FLUSH_AGGREGATION_JOB, - PAYMENT_SUCCESS_JOB, PAYMENT_FAILED_JOB, + PAYMENT_SUCCESS_JOB, + RESET_PASSWORD_JOB, SUBSCRIPTION_CANCELLED_JOB, } from "@/shared/constants/job"; -import { PaymentSuccessEmailDto } from "../email/dto/payment-success-email.dto"; +import { EMAIL_QUEUE, FEED_EVENT_QUEUE } from "@/shared/constants/queue"; +import { MagicLinkEmailDto } from "../email/dto/magic-link-email.dto"; import { PaymentFailedEmailDto } from "../email/dto/payment-failed-email.dto"; +import { PaymentSuccessEmailDto } from "../email/dto/payment-success-email.dto"; +import { ResetPasswordEmailDto } from "../email/dto/reset-password-email.dto"; import { SubscriptionCancelledEmailDto } from "../email/dto/subscription-cancelled-email.dto"; type QueueName = typeof EMAIL_QUEUE | typeof FEED_EVENT_QUEUE; diff --git a/src/shared/infra/upload/upload.controller.ts b/src/shared/infra/upload/upload.controller.ts index 7623947..f46b40d 100644 --- a/src/shared/infra/upload/upload.controller.ts +++ b/src/shared/infra/upload/upload.controller.ts @@ -1,8 +1,8 @@ import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common"; import { FileInterceptor } from "@nestjs/platform-express"; import { AuthGuard } from "@thallesp/nestjs-better-auth"; -import { UploadService } from "./upload.service"; import { imageConfig } from "./upload.config"; +import { UploadService } from "./upload.service"; @Controller("/upload") @UseGuards(AuthGuard) diff --git a/src/shared/infra/upload/upload.module.ts b/src/shared/infra/upload/upload.module.ts index 161d87a..3d23b5c 100644 --- a/src/shared/infra/upload/upload.module.ts +++ b/src/shared/infra/upload/upload.module.ts @@ -1,7 +1,6 @@ import { Global, Module } from "@nestjs/common"; - -import { UploadService } from "./upload.service"; import { UploadController } from "./upload.controller"; +import { UploadService } from "./upload.service"; @Global() @Module({ diff --git a/src/shared/utils/currency.ts b/src/shared/utils/currency.ts index 9b83dcb..c232479 100644 --- a/src/shared/utils/currency.ts +++ b/src/shared/utils/currency.ts @@ -1,6 +1,6 @@ import axios from "axios"; -import type { ClientIpType } from "../decorators/client-ip.decorator"; import { DEFAULT_CURRENCY } from "../constants/payment"; +import type { ClientIpType } from "../decorators/client-ip.decorator"; export function formatValue(value: number, currency: string) { return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(value); diff --git a/test/e2e/utils/test.ts b/test/e2e/utils/test.ts index da75bcf..854b023 100644 --- a/test/e2e/utils/test.ts +++ b/test/e2e/utils/test.ts @@ -1,4 +1,4 @@ -import { test as base, request as pwRequest, type APIRequestContext } from "@playwright/test"; +import { type APIRequestContext, test as base, request as pwRequest } from "@playwright/test"; export const test = base.extend({ api: [ diff --git a/test/http/trackgeek-api.http b/test/http/trackgeek-api.http index 85fb750..54734ee 100644 --- a/test/http/trackgeek-api.http +++ b/test/http/trackgeek-api.http @@ -405,10 +405,10 @@ GET {{apiUrl}}/tv/top?filter=popular GET {{apiUrl}}/tv/detail/1622 ### 59. TV Show Season -GET {{apiUrl}}/tv/detail/1622/season +GET {{apiUrl}}/tv/detail/79744/season ### 60. TV Show Season Episodes -GET {{apiUrl}}/tv/detail/1622/season/13/episode +GET {{apiUrl}}/tv/detail/79744/season/9/episode ### 61. Create TV Show Review POST {{apiUrl}}/tv/review @@ -802,6 +802,9 @@ GET {{apiUrl}}/list/user/{{login.response.body.user.id}} ### 103. Get List by Id GET {{apiUrl}}/list/019ce334-c8ac-7883-949d-948f53218272 +### 103.1 Get Lists Containing Item +GET {{apiUrl}}/list/containing?type=TVShow&tvShowId=019ce334-c8ac-7883-949d-948f53218272 + ### 104. Get List Items GET {{apiUrl}}/list/019ce334-c8ac-7883-949d-948f53218272/item diff --git a/test/load/load.ts b/test/load/load.ts index 85daed3..8d66f60 100644 --- a/test/load/load.ts +++ b/test/load/load.ts @@ -1,5 +1,5 @@ -import http from "k6/http"; import { check } from "k6"; +import http from "k6/http"; import { Rate, Trend } from "k6/metrics"; const errorRate = new Rate("errors"); diff --git a/test/unit/controllers/anime-progress.controller.spec.ts b/test/unit/controllers/anime-progress.controller.spec.ts index 3f74dd5..722ff44 100644 --- a/test/unit/controllers/anime-progress.controller.spec.ts +++ b/test/unit/controllers/anime-progress.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("AnimeProgressController", () => { it("true is true", () => { diff --git a/test/unit/controllers/anime-review.controller.spec.ts b/test/unit/controllers/anime-review.controller.spec.ts index 24840cc..15690a2 100644 --- a/test/unit/controllers/anime-review.controller.spec.ts +++ b/test/unit/controllers/anime-review.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("AnimeReviewController", () => { it("true is true", () => { diff --git a/test/unit/controllers/anime-watch.controller.spec.ts b/test/unit/controllers/anime-watch.controller.spec.ts index efa0941..eaf3a9c 100644 --- a/test/unit/controllers/anime-watch.controller.spec.ts +++ b/test/unit/controllers/anime-watch.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("AnimeWatchController", () => { it("true is true", () => { diff --git a/test/unit/controllers/anime.controller.spec.ts b/test/unit/controllers/anime.controller.spec.ts index 48a6de4..a589af1 100644 --- a/test/unit/controllers/anime.controller.spec.ts +++ b/test/unit/controllers/anime.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("AnimeController", () => { it("true is true", () => { diff --git a/test/unit/controllers/book-progress.controller.spec.ts b/test/unit/controllers/book-progress.controller.spec.ts index 8597025..971b67f 100644 --- a/test/unit/controllers/book-progress.controller.spec.ts +++ b/test/unit/controllers/book-progress.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("BookProgressController", () => { it("true is true", () => { diff --git a/test/unit/controllers/book-review.controller.spec.ts b/test/unit/controllers/book-review.controller.spec.ts index cce4bb9..65d4b28 100644 --- a/test/unit/controllers/book-review.controller.spec.ts +++ b/test/unit/controllers/book-review.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("BookReviewController", () => { it("true is true", () => { diff --git a/test/unit/controllers/book.controller.spec.ts b/test/unit/controllers/book.controller.spec.ts index e5a85b6..4d96a0e 100644 --- a/test/unit/controllers/book.controller.spec.ts +++ b/test/unit/controllers/book.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("BookController", () => { it("true is true", () => { diff --git a/test/unit/controllers/comment.controller.spec.ts b/test/unit/controllers/comment.controller.spec.ts index c6cff3f..d7f1c2d 100644 --- a/test/unit/controllers/comment.controller.spec.ts +++ b/test/unit/controllers/comment.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("CommentController", () => { it("true is true", () => { diff --git a/test/unit/controllers/favorite.controller.spec.ts b/test/unit/controllers/favorite.controller.spec.ts index 1ac4528..d990548 100644 --- a/test/unit/controllers/favorite.controller.spec.ts +++ b/test/unit/controllers/favorite.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("FavoriteController", () => { it("true is true", () => { diff --git a/test/unit/controllers/feed-event.controller.spec.ts b/test/unit/controllers/feed-event.controller.spec.ts index 28751e4..6820717 100644 --- a/test/unit/controllers/feed-event.controller.spec.ts +++ b/test/unit/controllers/feed-event.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("FeedEventController", () => { it("true is true", () => { diff --git a/test/unit/controllers/game-progress.controller.spec.ts b/test/unit/controllers/game-progress.controller.spec.ts index 45aa9c8..67c7bfb 100644 --- a/test/unit/controllers/game-progress.controller.spec.ts +++ b/test/unit/controllers/game-progress.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("GameProgressController", () => { it("true is true", () => { diff --git a/test/unit/controllers/game-review-screenshot.controller.spec.ts b/test/unit/controllers/game-review-screenshot.controller.spec.ts index e50f470..45e88c4 100644 --- a/test/unit/controllers/game-review-screenshot.controller.spec.ts +++ b/test/unit/controllers/game-review-screenshot.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { GameReviewScreenshotController } from "@/modules/game/controller/game-review-screenshot.controller"; const mockGetGameReviewScreenshots = vi.fn(); diff --git a/test/unit/controllers/game-review.controller.spec.ts b/test/unit/controllers/game-review.controller.spec.ts index 4e51a8a..2c2d3de 100644 --- a/test/unit/controllers/game-review.controller.spec.ts +++ b/test/unit/controllers/game-review.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("GameReviewController", () => { it("true is true", () => { diff --git a/test/unit/controllers/game.controller.spec.ts b/test/unit/controllers/game.controller.spec.ts index 5cb5ed0..a9a0944 100644 --- a/test/unit/controllers/game.controller.spec.ts +++ b/test/unit/controllers/game.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("GameController", () => { it("true is true", () => { diff --git a/test/unit/controllers/list.controller.spec.ts b/test/unit/controllers/list.controller.spec.ts index b67d815..3701b5c 100644 --- a/test/unit/controllers/list.controller.spec.ts +++ b/test/unit/controllers/list.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("ListController", () => { it("true is true", () => { diff --git a/test/unit/controllers/manga-progress.controller.spec.ts b/test/unit/controllers/manga-progress.controller.spec.ts index fe982b0..5e172af 100644 --- a/test/unit/controllers/manga-progress.controller.spec.ts +++ b/test/unit/controllers/manga-progress.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MangaProgressController", () => { it("true is true", () => { diff --git a/test/unit/controllers/manga-review.controller.spec.ts b/test/unit/controllers/manga-review.controller.spec.ts index 7c88628..632a5fd 100644 --- a/test/unit/controllers/manga-review.controller.spec.ts +++ b/test/unit/controllers/manga-review.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MangaReviewController", () => { it("true is true", () => { diff --git a/test/unit/controllers/manga.controller.spec.ts b/test/unit/controllers/manga.controller.spec.ts index db374e1..bb0d68b 100644 --- a/test/unit/controllers/manga.controller.spec.ts +++ b/test/unit/controllers/manga.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MangaController", () => { it("true is true", () => { diff --git a/test/unit/controllers/movie-progress.controller.spec.ts b/test/unit/controllers/movie-progress.controller.spec.ts index 0e9d62c..90727a9 100644 --- a/test/unit/controllers/movie-progress.controller.spec.ts +++ b/test/unit/controllers/movie-progress.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MovieProgressController", () => { it("true is true", () => { diff --git a/test/unit/controllers/movie-review.controller.spec.ts b/test/unit/controllers/movie-review.controller.spec.ts index d437ec8..4cd78db 100644 --- a/test/unit/controllers/movie-review.controller.spec.ts +++ b/test/unit/controllers/movie-review.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MovieReviewController", () => { it("true is true", () => { diff --git a/test/unit/controllers/movie.controller.spec.ts b/test/unit/controllers/movie.controller.spec.ts index d56eba3..40ca398 100644 --- a/test/unit/controllers/movie.controller.spec.ts +++ b/test/unit/controllers/movie.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MovieController", () => { it("true is true", () => { diff --git a/test/unit/controllers/profile.controller.spec.ts b/test/unit/controllers/profile.controller.spec.ts index 682aedf..b124a47 100644 --- a/test/unit/controllers/profile.controller.spec.ts +++ b/test/unit/controllers/profile.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("ProfileController", () => { it("true is true", () => { diff --git a/test/unit/controllers/reaction.controller.spec.ts b/test/unit/controllers/reaction.controller.spec.ts index 5df5372..1ce37d8 100644 --- a/test/unit/controllers/reaction.controller.spec.ts +++ b/test/unit/controllers/reaction.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("ReactionController", () => { it("true is true", () => { diff --git a/test/unit/controllers/tv-show-progress.controller.spec.ts b/test/unit/controllers/tv-show-progress.controller.spec.ts index 63ff2ea..1fb4f98 100644 --- a/test/unit/controllers/tv-show-progress.controller.spec.ts +++ b/test/unit/controllers/tv-show-progress.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("TVShowProgressController", () => { it("true is true", () => { diff --git a/test/unit/controllers/tv-show-review.controller.spec.ts b/test/unit/controllers/tv-show-review.controller.spec.ts index b30cf88..ef5207f 100644 --- a/test/unit/controllers/tv-show-review.controller.spec.ts +++ b/test/unit/controllers/tv-show-review.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("TVShowReviewController", () => { it("true is true", () => { diff --git a/test/unit/controllers/tv-show-watch.controller.spec.ts b/test/unit/controllers/tv-show-watch.controller.spec.ts index e18e529..8c0fedc 100644 --- a/test/unit/controllers/tv-show-watch.controller.spec.ts +++ b/test/unit/controllers/tv-show-watch.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("TVShowWatchController", () => { it("true is true", () => { diff --git a/test/unit/controllers/tv-show.controller.spec.ts b/test/unit/controllers/tv-show.controller.spec.ts index eafb548..e3e0e55 100644 --- a/test/unit/controllers/tv-show.controller.spec.ts +++ b/test/unit/controllers/tv-show.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("TVShowController", () => { it("true is true", () => { diff --git a/test/unit/controllers/user.controller.spec.ts b/test/unit/controllers/user.controller.spec.ts index 89b8070..1ab4351 100644 --- a/test/unit/controllers/user.controller.spec.ts +++ b/test/unit/controllers/user.controller.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("UserController", () => { it("true is true", () => { diff --git a/test/unit/filters/http-exception.filter.spec.ts b/test/unit/filters/http-exception.filter.spec.ts index cf9da27..bdde0d6 100644 --- a/test/unit/filters/http-exception.filter.spec.ts +++ b/test/unit/filters/http-exception.filter.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("HttpExceptionFilter", () => { it("true is true", () => { diff --git a/test/unit/guards/http-throttler.guard.spec.ts b/test/unit/guards/http-throttler.guard.spec.ts index 9509591..73bc55e 100644 --- a/test/unit/guards/http-throttler.guard.spec.ts +++ b/test/unit/guards/http-throttler.guard.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("HttpThrottlerGuard", () => { it("true is true", () => { diff --git a/test/unit/infra/cache.service.spec.ts b/test/unit/infra/cache.service.spec.ts index 30e92fa..04a261a 100644 --- a/test/unit/infra/cache.service.spec.ts +++ b/test/unit/infra/cache.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("CacheService", () => { it("true is true", () => { diff --git a/test/unit/infra/database.service.spec.ts b/test/unit/infra/database.service.spec.ts index fee5d23..95efcad 100644 --- a/test/unit/infra/database.service.spec.ts +++ b/test/unit/infra/database.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("DatabaseService", () => { it("true is true", () => { diff --git a/test/unit/infra/email.service.spec.ts b/test/unit/infra/email.service.spec.ts index 69bc4b6..cafbd1c 100644 --- a/test/unit/infra/email.service.spec.ts +++ b/test/unit/infra/email.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("EmailService", () => { it("true is true", () => { diff --git a/test/unit/infra/integrations/hardcover.service.spec.ts b/test/unit/infra/integrations/hardcover.service.spec.ts index c6b20cb..1c418c4 100644 --- a/test/unit/infra/integrations/hardcover.service.spec.ts +++ b/test/unit/infra/integrations/hardcover.service.spec.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; import { of, throwError } from "rxjs"; -import { HardcoverService } from "@/shared/infra/integrations/hardcover.service"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { AppException } from "@/shared/exceptions/app.exceptions"; +import { HardcoverService } from "@/shared/infra/integrations/hardcover.service"; const mockHttpService = { post: vi.fn(), diff --git a/test/unit/infra/integrations/igdb.service.spec.ts b/test/unit/infra/integrations/igdb.service.spec.ts index a365f21..0a76e0c 100644 --- a/test/unit/infra/integrations/igdb.service.spec.ts +++ b/test/unit/infra/integrations/igdb.service.spec.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; import { of, throwError } from "rxjs"; -import { IGDBService } from "@/shared/infra/integrations/igdb.service"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { AppException } from "@/shared/exceptions/app.exceptions"; +import { IGDBService } from "@/shared/infra/integrations/igdb.service"; const mockHttpService = { get: vi.fn(), diff --git a/test/unit/infra/integrations/imgbb.service.spec.ts b/test/unit/infra/integrations/imgbb.service.spec.ts index 8495fd4..21c81d2 100644 --- a/test/unit/infra/integrations/imgbb.service.spec.ts +++ b/test/unit/infra/integrations/imgbb.service.spec.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; import { of, throwError } from "rxjs"; -import { IMGBBService } from "@/shared/infra/integrations/imgbb.service"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { AppException } from "@/shared/exceptions/app.exceptions"; +import { IMGBBService } from "@/shared/infra/integrations/imgbb.service"; const mockHttpService = { post: vi.fn(), diff --git a/test/unit/infra/integrations/jikan.service.spec.ts b/test/unit/infra/integrations/jikan.service.spec.ts index 761e9db..cd4e6ba 100644 --- a/test/unit/infra/integrations/jikan.service.spec.ts +++ b/test/unit/infra/integrations/jikan.service.spec.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; import { of, throwError } from "rxjs"; -import { JikanService } from "@/shared/infra/integrations/jikan.service"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { AppException } from "@/shared/exceptions/app.exceptions"; +import { JikanService } from "@/shared/infra/integrations/jikan.service"; vi.mock("@/shared/utils/request", () => ({ manyRequestWithDelay: vi.fn(), diff --git a/test/unit/infra/integrations/tmdb.service.spec.ts b/test/unit/infra/integrations/tmdb.service.spec.ts index 2e0fddf..befc477 100644 --- a/test/unit/infra/integrations/tmdb.service.spec.ts +++ b/test/unit/infra/integrations/tmdb.service.spec.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; import { of, throwError } from "rxjs"; -import { TMDBService } from "@/shared/infra/integrations/tmdb.service"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { AppException } from "@/shared/exceptions/app.exceptions"; +import { TMDBService } from "@/shared/infra/integrations/tmdb.service"; const mockHttpService = { get: vi.fn(), diff --git a/test/unit/infra/queue/email.processor.spec.ts b/test/unit/infra/queue/email.processor.spec.ts index 1774da7..53fbcd2 100644 --- a/test/unit/infra/queue/email.processor.spec.ts +++ b/test/unit/infra/queue/email.processor.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("EmailProcessor", () => { it("true is true", () => { diff --git a/test/unit/infra/queue/feed-event.processor.spec.ts b/test/unit/infra/queue/feed-event.processor.spec.ts index a79702f..f3291b1 100644 --- a/test/unit/infra/queue/feed-event.processor.spec.ts +++ b/test/unit/infra/queue/feed-event.processor.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("FeedEventProcessor", () => { it("true is true", () => { diff --git a/test/unit/infra/queue/queue-service.spec.ts b/test/unit/infra/queue/queue-service.spec.ts index ddaada8..4ddb007 100644 --- a/test/unit/infra/queue/queue-service.spec.ts +++ b/test/unit/infra/queue/queue-service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("QueueService", () => { it("true is true", () => { diff --git a/test/unit/infra/upload.service.spec.ts b/test/unit/infra/upload.service.spec.ts index 7691602..70e16d9 100644 --- a/test/unit/infra/upload.service.spec.ts +++ b/test/unit/infra/upload.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("UploadService", () => { it("true is true", () => { diff --git a/test/unit/interceptors/metrics.interceptor.spec.ts b/test/unit/interceptors/metrics.interceptor.spec.ts index 26609fe..45d483a 100644 --- a/test/unit/interceptors/metrics.interceptor.spec.ts +++ b/test/unit/interceptors/metrics.interceptor.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MetricsInterceptor", () => { it("true is true", () => { diff --git a/test/unit/services/anime-progress.service.spec.ts b/test/unit/services/anime-progress.service.spec.ts index 5ab6f0f..ed7cac0 100644 --- a/test/unit/services/anime-progress.service.spec.ts +++ b/test/unit/services/anime-progress.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("AnimeProgressService", () => { it("true is true", () => { diff --git a/test/unit/services/anime-review.service.spec.ts b/test/unit/services/anime-review.service.spec.ts index 1478abd..4224c0a 100644 --- a/test/unit/services/anime-review.service.spec.ts +++ b/test/unit/services/anime-review.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("AnimeReviewService", () => { it("true is true", () => { diff --git a/test/unit/services/anime-watch.service.spec.ts b/test/unit/services/anime-watch.service.spec.ts index 80aade9..00bd20c 100644 --- a/test/unit/services/anime-watch.service.spec.ts +++ b/test/unit/services/anime-watch.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("AnimeWatchService", () => { it("true is true", () => { diff --git a/test/unit/services/anime.service.spec.ts b/test/unit/services/anime.service.spec.ts index d2922db..c7926af 100644 --- a/test/unit/services/anime.service.spec.ts +++ b/test/unit/services/anime.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("AnimeService", () => { it("true is true", () => { diff --git a/test/unit/services/book-progress.service.spec.ts b/test/unit/services/book-progress.service.spec.ts index 9e20437..9bcf992 100644 --- a/test/unit/services/book-progress.service.spec.ts +++ b/test/unit/services/book-progress.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("BookProgressService", () => { it("true is true", () => { diff --git a/test/unit/services/book-review.service.spec.ts b/test/unit/services/book-review.service.spec.ts index ff73963..6c3397f 100644 --- a/test/unit/services/book-review.service.spec.ts +++ b/test/unit/services/book-review.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("BookReviewService", () => { it("true is true", () => { diff --git a/test/unit/services/book.service.spec.ts b/test/unit/services/book.service.spec.ts index 1dd9f71..0e657ba 100644 --- a/test/unit/services/book.service.spec.ts +++ b/test/unit/services/book.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("BookService", () => { it("true is true", () => { diff --git a/test/unit/services/comment.service.spec.ts b/test/unit/services/comment.service.spec.ts index 3d5f7e0..acb80d9 100644 --- a/test/unit/services/comment.service.spec.ts +++ b/test/unit/services/comment.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("CommentService", () => { it("true is true", () => { diff --git a/test/unit/services/favorite.service.spec.ts b/test/unit/services/favorite.service.spec.ts index c91fa06..7ce2016 100644 --- a/test/unit/services/favorite.service.spec.ts +++ b/test/unit/services/favorite.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("FavoriteService", () => { it("true is true", () => { diff --git a/test/unit/services/feed-event.service.spec.ts b/test/unit/services/feed-event.service.spec.ts index d96a8c4..c2b0739 100644 --- a/test/unit/services/feed-event.service.spec.ts +++ b/test/unit/services/feed-event.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("FeedEventService", () => { it("true is true", () => { diff --git a/test/unit/services/game-progress.service.spec.ts b/test/unit/services/game-progress.service.spec.ts index 00fb21e..2f6eca8 100644 --- a/test/unit/services/game-progress.service.spec.ts +++ b/test/unit/services/game-progress.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("GameProgressService", () => { it("true is true", () => { diff --git a/test/unit/services/game-review-screenshot.service.spec.ts b/test/unit/services/game-review-screenshot.service.spec.ts index c9582a8..1aa9ccf 100644 --- a/test/unit/services/game-review-screenshot.service.spec.ts +++ b/test/unit/services/game-review-screenshot.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { GameReviewScreenshotService } from "@/modules/game/service/game-review-screenshot.service"; import { ERROR_CODES } from "@/shared/constants/error-codes"; diff --git a/test/unit/services/game-review.service.spec.ts b/test/unit/services/game-review.service.spec.ts index b517ad6..c4f0c9f 100644 --- a/test/unit/services/game-review.service.spec.ts +++ b/test/unit/services/game-review.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("GameReviewService", () => { it("true is true", () => { diff --git a/test/unit/services/game.service.spec.ts b/test/unit/services/game.service.spec.ts index dffda84..10643d5 100644 --- a/test/unit/services/game.service.spec.ts +++ b/test/unit/services/game.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("GameService", () => { it("true is true", () => { diff --git a/test/unit/services/list.service.spec.ts b/test/unit/services/list.service.spec.ts index e026764..35278e6 100644 --- a/test/unit/services/list.service.spec.ts +++ b/test/unit/services/list.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("ListService", () => { it("true is true", () => { diff --git a/test/unit/services/manga-progress.service.spec.ts b/test/unit/services/manga-progress.service.spec.ts index 946565a..a495f07 100644 --- a/test/unit/services/manga-progress.service.spec.ts +++ b/test/unit/services/manga-progress.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MangaProgressService", () => { it("true is true", () => { diff --git a/test/unit/services/manga-review.service.spec.ts b/test/unit/services/manga-review.service.spec.ts index e3bbecc..eb0f6d3 100644 --- a/test/unit/services/manga-review.service.spec.ts +++ b/test/unit/services/manga-review.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MangaReviewService", () => { it("true is true", () => { diff --git a/test/unit/services/manga.service.spec.ts b/test/unit/services/manga.service.spec.ts index 2cb4743..016fd13 100644 --- a/test/unit/services/manga.service.spec.ts +++ b/test/unit/services/manga.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MangaService", () => { it("true is true", () => { diff --git a/test/unit/services/movie-progress.service.spec.ts b/test/unit/services/movie-progress.service.spec.ts index 042ab94..96d440a 100644 --- a/test/unit/services/movie-progress.service.spec.ts +++ b/test/unit/services/movie-progress.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MovieProgressService", () => { it("true is true", () => { diff --git a/test/unit/services/movie-review.service.spec.ts b/test/unit/services/movie-review.service.spec.ts index 05e6898..8c4bc26 100644 --- a/test/unit/services/movie-review.service.spec.ts +++ b/test/unit/services/movie-review.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MovieReviewService", () => { it("true is true", () => { diff --git a/test/unit/services/movie.service.spec.ts b/test/unit/services/movie.service.spec.ts index d93fb89..670fb8c 100644 --- a/test/unit/services/movie.service.spec.ts +++ b/test/unit/services/movie.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("MovieService", () => { it("true is true", () => { diff --git a/test/unit/services/profile.service.spec.ts b/test/unit/services/profile.service.spec.ts index 5fd660a..501802a 100644 --- a/test/unit/services/profile.service.spec.ts +++ b/test/unit/services/profile.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("ProfileService", () => { it("true is true", () => { diff --git a/test/unit/services/reaction.service.spec.ts b/test/unit/services/reaction.service.spec.ts index 8ca5f41..6a1e149 100644 --- a/test/unit/services/reaction.service.spec.ts +++ b/test/unit/services/reaction.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("ReactionService", () => { it("true is true", () => { diff --git a/test/unit/services/tv-show-progress.service.spec.ts b/test/unit/services/tv-show-progress.service.spec.ts index d74ab34..b1a7ecb 100644 --- a/test/unit/services/tv-show-progress.service.spec.ts +++ b/test/unit/services/tv-show-progress.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("TVShowProgressService", () => { it("true is true", () => { diff --git a/test/unit/services/tv-show-review.service.spec.ts b/test/unit/services/tv-show-review.service.spec.ts index 44ead3a..4284c54 100644 --- a/test/unit/services/tv-show-review.service.spec.ts +++ b/test/unit/services/tv-show-review.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("TVShowReviewService", () => { it("true is true", () => { diff --git a/test/unit/services/tv-show-watch.service.spec.ts b/test/unit/services/tv-show-watch.service.spec.ts index c20c43a..d3e160e 100644 --- a/test/unit/services/tv-show-watch.service.spec.ts +++ b/test/unit/services/tv-show-watch.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("TVShowWatchService", () => { it("true is true", () => { diff --git a/test/unit/services/tv-show.service.spec.ts b/test/unit/services/tv-show.service.spec.ts index a6d8c25..98acbec 100644 --- a/test/unit/services/tv-show.service.spec.ts +++ b/test/unit/services/tv-show.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("TVShowService", () => { it("true is true", () => { diff --git a/test/unit/services/user.service.spec.ts b/test/unit/services/user.service.spec.ts index 1d21ec0..b6b7d3b 100644 --- a/test/unit/services/user.service.spec.ts +++ b/test/unit/services/user.service.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; describe("UserService", () => { it("true is true", () => {