From 5dea878eba13de60611b00dcdcfab095ed07dc17 Mon Sep 17 00:00:00 2001 From: Chris Pinkney Date: Mon, 26 Feb 2024 21:09:37 -0500 Subject: [PATCH] feat(api): implement movies module --- api/.eslintrc.js | 2 +- api/src/app.module.ts | 10 +- api/src/lists/daos/list.dao.ts | 119 +------------------ api/src/lists/dtos/create-list-return.dto.ts | 33 +---- api/src/lists/guards/list.guard.ts | 3 +- api/src/lists/lists.controller.ts | 8 -- api/src/lists/lists.module.ts | 7 +- api/src/lists/lists.service.ts | 31 +++-- api/src/movies/dao/movies.dao.ts | 91 ++++++++++++++ api/src/movies/movies.controller.spec.ts | 18 +++ api/src/movies/movies.controller.ts | 50 ++++++++ api/src/movies/movies.module.ts | 13 ++ api/src/movies/movies.service.spec.ts | 18 +++ api/src/movies/movies.service.ts | 34 ++++++ api/src/users/guards/comment-auth.guard.ts | 4 +- api/src/users/users.module.ts | 8 +- 16 files changed, 275 insertions(+), 174 deletions(-) create mode 100644 api/src/movies/dao/movies.dao.ts create mode 100644 api/src/movies/movies.controller.spec.ts create mode 100644 api/src/movies/movies.controller.ts create mode 100644 api/src/movies/movies.module.ts create mode 100644 api/src/movies/movies.service.spec.ts create mode 100644 api/src/movies/movies.service.ts diff --git a/api/.eslintrc.js b/api/.eslintrc.js index f62112f..ec556a8 100644 --- a/api/.eslintrc.js +++ b/api/.eslintrc.js @@ -20,6 +20,6 @@ module.exports = { '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-unused-vars': 'warn', }, }; diff --git a/api/src/app.module.ts b/api/src/app.module.ts index b4645f5..7b6139c 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -10,9 +10,17 @@ import { UsersModule } from './users/users.module'; import { ListsModule } from './lists/lists.module'; import { EmailModule } from './email/email.module'; import { ImageModule } from './image/image.module'; +import { MoviesModule } from './movies/movies.module'; @Module({ - imports: [ConfigModule, UsersModule, ListsModule, EmailModule, ImageModule], + imports: [ + ConfigModule, + UsersModule, + ListsModule, + EmailModule, + ImageModule, + MoviesModule, + ], controllers: [AppController], providers: [ { diff --git a/api/src/lists/daos/list.dao.ts b/api/src/lists/daos/list.dao.ts index 67427fb..d46c41d 100644 --- a/api/src/lists/daos/list.dao.ts +++ b/api/src/lists/daos/list.dao.ts @@ -2,11 +2,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { v4 as uuidv4 } from 'uuid'; import { PrismaService } from '../../prisma/prisma.service'; import { UpdateListDto } from '../dtos/update-list.dto'; -import { CreateListDto } from '../dtos/create-list.dto'; import { Role, User } from '@prisma/client'; @Injectable() -export class ListsDao { +export class ListDao { constructor(private readonly prisma: PrismaService) {} async getPublicList(listId: string) { @@ -112,67 +111,18 @@ export class ListsDao { }); } - async createList(createList: CreateListDto, userId: string) { - let newMovies; - const list = await this.prisma.list.create({ + async createList(listName: string, userId: string) { + return await this.prisma.list.create({ data: { id: uuidv4(), - name: createList.name, + name: listName, isPrivate: true, creatorId: userId, user: { connect: { id: userId }, }, }, - include: { - movie: true, - }, }); - - if (createList?.movie?.length) { - newMovies = await Promise.all( - createList.movie.map((movie) => - this.prisma.movie.upsert({ - where: { imdbId: movie.imdbId }, - create: { - id: uuidv4(), - title: movie.title, - description: movie.description, - genre: movie.genre, - releaseDate: movie.releaseDate, - posterUrl: movie.posterUrl, - rating: movie.rating, - imdbId: movie.imdbId, - }, - update: {}, - }), - ), - ); - - await Promise.all( - newMovies.map((movie) => - this.prisma.movie.update({ - where: { id: movie.id }, - data: { - list: { - connect: { - id: list.id, - }, - }, - }, - }), - ), - ); - } - - const createdList = await this.prisma.list.findUniqueOrThrow({ - where: { id: list.id }, - include: { - movie: true, - }, - }); - - return createdList; } async updateListPrivacy(listId: string) { @@ -190,44 +140,6 @@ export class ListsDao { } async updateList(updateListDto: UpdateListDto) { - let newMovies = []; - - if (updateListDto?.movie?.length) { - newMovies = await Promise.all( - updateListDto.movie.map((movie) => - this.prisma.movie.upsert({ - where: { imdbId: movie.imdbId }, - create: { - id: uuidv4(), - title: movie.title, - description: movie.description, - genre: movie.genre, - releaseDate: movie.releaseDate, - posterUrl: movie.posterUrl, - rating: movie.rating, - imdbId: movie.imdbId, - }, - update: {}, - }), - ), - ); - - await Promise.all( - newMovies.map((movie) => - this.prisma.movie.update({ - where: { id: movie.id }, - data: { - list: { - connect: { - id: updateListDto.listId, - }, - }, - }, - }), - ), - ); - } - return await this.prisma.list.update({ where: { id: updateListDto.listId }, data: { @@ -237,29 +149,6 @@ export class ListsDao { }); } - async updateWatchedStatus(movieId: string, userId: string) { - const user = await this.getWatchedMovies(userId); - - const hasWatched = user.movie.find( - (watchedMovie) => watchedMovie.id === movieId, - ); - - return await this.prisma.user.update({ - where: { id: userId }, - data: { - movie: { - ...(hasWatched - ? { - disconnect: { id: movieId }, - } - : { - connect: { id: movieId }, - }), - }, - }, - }); - } - async deleteList(listId: string, user: User) { const list = await this.prisma.list.findUniqueOrThrow({ where: { id: listId }, diff --git a/api/src/lists/dtos/create-list-return.dto.ts b/api/src/lists/dtos/create-list-return.dto.ts index 4a6ee91..a3a2d06 100644 --- a/api/src/lists/dtos/create-list-return.dto.ts +++ b/api/src/lists/dtos/create-list-return.dto.ts @@ -3,33 +3,7 @@ import { ValidateNested } from 'class-validator'; import { UserDto } from '../../users/dtos/user.dto'; import { CommentDto } from './comments.dto'; -class Movie { - @Expose() - id: number; - - @Expose() - title: string; - - @Expose() - description?: string; - - @Expose() - genre: string[]; - - @Expose() - releaseDate: string; - - @Expose() - posterUrl: string; - - @Expose() - rating: string; - - @Expose() - imdbId: string; -} - -class List { +export class List { @Expose() id: number; @@ -48,11 +22,6 @@ class List { @Expose() updatedAt: Date; - @Expose() - @Type(() => Movie) - @ValidateNested() - movie: Movie[]; - @Expose() @Type(() => UserDto) @ValidateNested() diff --git a/api/src/lists/guards/list.guard.ts b/api/src/lists/guards/list.guard.ts index 3e6fa55..7bf82f4 100644 --- a/api/src/lists/guards/list.guard.ts +++ b/api/src/lists/guards/list.guard.ts @@ -10,7 +10,8 @@ export class ListAuthGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const { user } = request; - const listId = request.params.id || request.body.listId; + const listId = + request.params.id || request.body.listId || request.params.listId; const list = await this.prisma.list.findUniqueOrThrow({ where: { id: listId }, diff --git a/api/src/lists/lists.controller.ts b/api/src/lists/lists.controller.ts index 06fd2f4..63ede41 100644 --- a/api/src/lists/lists.controller.ts +++ b/api/src/lists/lists.controller.ts @@ -191,14 +191,6 @@ export class ListsController { return this.listsService.updateList(body); } - @UseInterceptors(RemoveListFieldsInterceptor) - @Patch('/movies/:id/updateWatchedStatus') - @HttpCode(HttpStatus.NO_CONTENT) - updateWatchedStatus(@Param('id') movieId: string, @Req() req: Request) { - if (!req.user) throw new BadRequestException('req contains no user'); - return this.listsService.updateWatchedStatus(movieId, req.user.id); - } - @UseInterceptors(RemoveListFieldsInterceptor) @UseGuards(ListAuthGuard) @Delete('/:id') diff --git a/api/src/lists/lists.module.ts b/api/src/lists/lists.module.ts index fb20e97..5926ee2 100644 --- a/api/src/lists/lists.module.ts +++ b/api/src/lists/lists.module.ts @@ -5,17 +5,18 @@ import { UsersService } from '../users/users.service'; import { PrismaModule } from '../prisma/prisma.module'; import { EmailModule } from '../email/email.module'; import { UsersModule } from '../users/users.module'; -import { ListsDao } from './daos/list.dao'; +import { ListDao } from './daos/list.dao'; import { CommentDao } from './daos/comment.dao'; import { UsersDao } from '../users/daos/user.dao'; import { CommentsService } from './comments.service'; +import { MoviesModule } from '../movies/movies.module'; @Module({ - imports: [PrismaModule, EmailModule, UsersModule], + imports: [PrismaModule, EmailModule, UsersModule, MoviesModule], controllers: [ListsController], providers: [ ListsService, - ListsDao, + ListDao, UsersService, UsersDao, CommentsService, diff --git a/api/src/lists/lists.service.ts b/api/src/lists/lists.service.ts index 87943da..f4a3751 100644 --- a/api/src/lists/lists.service.ts +++ b/api/src/lists/lists.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@nestjs/common'; import { CreateListDto } from './dtos/create-list.dto'; import { UpdateListDto } from './dtos/update-list.dto'; -import { ListsDao } from './daos/list.dao'; +import { ListDao } from './daos/list.dao'; import { UsersService } from '../users/users.service'; import { EmailService } from '../email/email.service'; import { CloneListDto } from './dtos/clone-list.dto'; +import { MoviesService } from '../movies/movies.service'; interface Comment { id: string; @@ -18,9 +19,10 @@ interface Comment { @Injectable() export class ListsService { constructor( - private listsDao: ListsDao, + private listsDao: ListDao, private usersService: UsersService, private emailService: EmailService, + private moviesService: MoviesService, ) {} async getPublicList(listId: string) { @@ -90,8 +92,12 @@ export class ListsService { return userListDetails; } - async createList(createList: CreateListDto, userId: string) { - return await this.listsDao.createList(createList, userId); + async createList(body: CreateListDto, userId: string) { + const list = await this.listsDao.createList(body.name, userId); + + await this.moviesService.createMovies(body.movie, list.id); + + return await this.listsDao.getList(list.id); } async cloneList(cloneList: CloneListDto, userId: string) { @@ -110,7 +116,13 @@ export class ListsService { })), }; - const clonedList = await this.listsDao.createList(clonedListData, userId); + const clonedList = await this.createList( + { + name: clonedListData.name, + movie: clonedListData.movie, + }, + userId, + ); return clonedList; } @@ -120,11 +132,12 @@ export class ListsService { } async updateList(updateListDto: UpdateListDto) { - return await this.listsDao.updateList(updateListDto); - } + await this.moviesService.createMovies( + updateListDto.movie, + updateListDto.listId, + ); - async updateWatchedStatus(movieId: string, userId: string) { - return await this.listsDao.updateWatchedStatus(movieId, userId); + return await this.listsDao.updateList(updateListDto); } async deleteList(listId: string, userId: string) { diff --git a/api/src/movies/dao/movies.dao.ts b/api/src/movies/dao/movies.dao.ts new file mode 100644 index 0000000..14f166b --- /dev/null +++ b/api/src/movies/dao/movies.dao.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { PrismaService } from '../../prisma/prisma.service'; +import { MovieDto } from '../../lists/dtos/movie.dto'; + +@Injectable() +export class MoviesDao { + constructor(private readonly prisma: PrismaService) {} + + async createMovies(movies: MovieDto[], listId: string) { + let newMovies; + + if (movies?.length) { + newMovies = await Promise.all( + movies.map((movie) => + this.prisma.movie.upsert({ + where: { imdbId: movie.imdbId }, + create: { + id: uuidv4(), + title: movie.title, + description: movie.description, + genre: movie.genre, + releaseDate: movie.releaseDate, + posterUrl: movie.posterUrl, + rating: movie.rating, + imdbId: movie.imdbId, + }, + update: {}, + }), + ), + ); + + await Promise.all( + newMovies.map((movie) => + this.prisma.movie.update({ + where: { id: movie.id }, + data: { + list: { + connect: { + id: listId, + }, + }, + }, + }), + ), + ); + } + + return newMovies; + } + + getMovies(movieId?: string, userId?: string) { + return this.prisma.movie.findMany({ + where: { + AND: [{ user: { some: { id: userId } } }, { id: movieId }], + }, + }); + } + + async updateWatchedStatus( + movieId: string, + userId: string, + hasWatched: boolean, + ) { + return await this.prisma.movie.update({ + where: { id: movieId }, + data: { + user: { + ...(hasWatched + ? { + disconnect: { id: userId }, + } + : { + connect: { id: userId }, + }), + }, + }, + }); + } + + async removeMovieFromList(listId: string, movieId: string) { + await this.prisma.movie.update({ + where: { id: movieId }, + data: { + list: { + disconnect: { id: listId }, + }, + }, + }); + } +} diff --git a/api/src/movies/movies.controller.spec.ts b/api/src/movies/movies.controller.spec.ts new file mode 100644 index 0000000..4f6a8ee --- /dev/null +++ b/api/src/movies/movies.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MoviesController } from './movies.controller'; + +describe('MoviesController', () => { + let controller: MoviesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MoviesController], + }).compile(); + + controller = module.get(MoviesController); + }); + + it.skip('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/api/src/movies/movies.controller.ts b/api/src/movies/movies.controller.ts new file mode 100644 index 0000000..7690b70 --- /dev/null +++ b/api/src/movies/movies.controller.ts @@ -0,0 +1,50 @@ +import { + Controller, + Get, + Patch, + Delete, + UseGuards, + UseInterceptors, + HttpCode, + HttpStatus, + Req, + BadRequestException, + Param, + Query, +} from '@nestjs/common'; +import { Request } from 'express'; +import { ListAuthGuard } from '../lists/guards/list.guard'; +import { RemoveListFieldsInterceptor } from '../lists/interceptors/remove-list-fields.interceptor'; +import { MoviesService } from './movies.service'; + +@Controller('movies') +export class MoviesController { + constructor(private moviesService: MoviesService) {} + + @Get('/') + async getMovies(@Query('movieId') movieId: string, @Req() req: Request) { + if (!req.user) throw new BadRequestException('req contains no user'); + return this.moviesService.getMovies(movieId, req.user.id); + } + + @UseInterceptors(RemoveListFieldsInterceptor) + @Patch('/:id') + @HttpCode(HttpStatus.NO_CONTENT) + updateWatchedStatus(@Param('id') movieId: string, @Req() req: Request) { + if (!req.user) throw new BadRequestException('req contains no user'); + return this.moviesService.updateWatchedStatus(movieId, req.user.id); + } + + @UseInterceptors(RemoveListFieldsInterceptor) + @UseGuards(ListAuthGuard) + @Delete('/:id/lists/:listId') + @HttpCode(HttpStatus.NO_CONTENT) + removeMovieFromList( + @Param('listId') listId: string, + @Param('id') movieId: string, + @Req() req: Request, + ) { + if (!req.user) throw new BadRequestException('req contains no user'); + return this.moviesService.removeMovieFromList(listId, movieId); + } +} diff --git a/api/src/movies/movies.module.ts b/api/src/movies/movies.module.ts new file mode 100644 index 0000000..48abdce --- /dev/null +++ b/api/src/movies/movies.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { MoviesController } from './movies.controller'; +import { PrismaModule } from '../prisma/prisma.module'; +import { MoviesService } from './movies.service'; +import { MoviesDao } from './dao/movies.dao'; + +@Module({ + imports: [PrismaModule], + providers: [MoviesService, MoviesDao], + controllers: [MoviesController], + exports: [MoviesService], +}) +export class MoviesModule {} diff --git a/api/src/movies/movies.service.spec.ts b/api/src/movies/movies.service.spec.ts new file mode 100644 index 0000000..3e12e00 --- /dev/null +++ b/api/src/movies/movies.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MoviesService } from './movies.service'; + +describe('MoviesService', () => { + let service: MoviesService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MoviesService], + }).compile(); + + service = module.get(MoviesService); + }); + + it.skip('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/api/src/movies/movies.service.ts b/api/src/movies/movies.service.ts new file mode 100644 index 0000000..0734bcf --- /dev/null +++ b/api/src/movies/movies.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { MoviesDao } from './dao/movies.dao'; +import { MovieDto } from '../lists/dtos/movie.dto'; + +@Injectable() +export class MoviesService { + constructor(private moviesDao: MoviesDao) {} + + async getMovies(movieId?: string, userId?: string) { + return this.moviesDao.getMovies(movieId, userId); + } + + async createMovies(movies: MovieDto[], listId: string) { + return await this.moviesDao.createMovies(movies, listId); + } + + async updateWatchedStatus(movieId: string, userId: string) { + const watchedMovies = await this.getMovies(movieId, userId); + + const hasWatched = !!watchedMovies.find( + (watchedMovie) => watchedMovie.id === movieId, + ); + + return await this.moviesDao.updateWatchedStatus( + movieId, + userId, + hasWatched, + ); + } + + async removeMovieFromList(listId: string, movieId: string) { + return await this.moviesDao.removeMovieFromList(listId, movieId); + } +} diff --git a/api/src/users/guards/comment-auth.guard.ts b/api/src/users/guards/comment-auth.guard.ts index 650fdb3..174d267 100644 --- a/api/src/users/guards/comment-auth.guard.ts +++ b/api/src/users/guards/comment-auth.guard.ts @@ -1,9 +1,9 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; -import { ListsDao } from '../../lists/daos/list.dao'; +import { ListDao } from '../../lists/daos/list.dao'; @Injectable() export class CommentAuthorizationGuard implements CanActivate { - constructor(private listsDao: ListsDao) {} + constructor(private listsDao: ListDao) {} async canActivate(context: ExecutionContext) { const request = context.switchToHttp().getRequest(); diff --git a/api/src/users/users.module.ts b/api/src/users/users.module.ts index 25540a8..b878aa8 100644 --- a/api/src/users/users.module.ts +++ b/api/src/users/users.module.ts @@ -8,7 +8,7 @@ import { PrismaModule } from '../prisma/prisma.module'; import { EmailModule } from '../email/email.module'; import { UsersDao } from './daos/user.dao'; import { ListsService } from '../lists/lists.service'; -import { ListsDao } from '../lists/daos/list.dao'; +import { ListDao } from '../lists/daos/list.dao'; import { LocalStrategy } from './auth/passport/local.strategy'; import { JwtStrategy } from './auth/passport/jwt.strategy'; import { CommentDao } from '../lists/daos/comment.dao'; @@ -18,6 +18,7 @@ import { ExportModule } from './export/export.module'; import { ImageModule } from '../image/image.module'; import { ImageService } from '../image/image.service'; import { ImageDao } from '../image/daos/image.dao'; +import { MoviesModule } from '../movies/movies.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { ImageDao } from '../image/daos/image.dao'; AuthModule, ExportModule, ImageModule, + MoviesModule, ], controllers: [UsersController], providers: [ @@ -37,11 +39,13 @@ import { ImageDao } from '../image/daos/image.dao'; ListsService, JwtService, UsersDao, - ListsDao, + ListDao, CommentDao, ImageDao, LocalStrategy, JwtStrategy, + UsersService, ], + exports: [UsersService], }) export class UsersModule {}