From 1a8198b8490b97f0e8673d4a9a594b4fb21d608b Mon Sep 17 00:00:00 2001 From: "R. A. Souza" Date: Sat, 6 Apr 2024 12:21:41 +0200 Subject: [PATCH 1/7] feat(api): :sparkles: generate movies module --- apps/api/src/movies/dto/create-movie.dto.ts | 1 + apps/api/src/movies/dto/update-movie.dto.ts | 4 +++ apps/api/src/movies/entities/movie.entity.ts | 1 + .../src/movies/interfaces/search.interface.ts | 3 ++ apps/api/src/movies/movies.controller.spec.ts | 20 +++++++++++ apps/api/src/movies/movies.controller.ts | 34 +++++++++++++++++++ apps/api/src/movies/movies.module.ts | 9 +++++ apps/api/src/movies/movies.service.spec.ts | 18 ++++++++++ apps/api/src/movies/movies.service.ts | 26 ++++++++++++++ 9 files changed, 116 insertions(+) create mode 100644 apps/api/src/movies/dto/create-movie.dto.ts create mode 100644 apps/api/src/movies/dto/update-movie.dto.ts create mode 100644 apps/api/src/movies/entities/movie.entity.ts create mode 100644 apps/api/src/movies/interfaces/search.interface.ts create mode 100644 apps/api/src/movies/movies.controller.spec.ts create mode 100644 apps/api/src/movies/movies.controller.ts create mode 100644 apps/api/src/movies/movies.module.ts create mode 100644 apps/api/src/movies/movies.service.spec.ts create mode 100644 apps/api/src/movies/movies.service.ts diff --git a/apps/api/src/movies/dto/create-movie.dto.ts b/apps/api/src/movies/dto/create-movie.dto.ts new file mode 100644 index 0000000..4124da8 --- /dev/null +++ b/apps/api/src/movies/dto/create-movie.dto.ts @@ -0,0 +1 @@ +export class CreateMovieDto {} diff --git a/apps/api/src/movies/dto/update-movie.dto.ts b/apps/api/src/movies/dto/update-movie.dto.ts new file mode 100644 index 0000000..7efc78d --- /dev/null +++ b/apps/api/src/movies/dto/update-movie.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateMovieDto } from './create-movie.dto'; + +export class UpdateMovieDto extends PartialType(CreateMovieDto) {} diff --git a/apps/api/src/movies/entities/movie.entity.ts b/apps/api/src/movies/entities/movie.entity.ts new file mode 100644 index 0000000..716ece6 --- /dev/null +++ b/apps/api/src/movies/entities/movie.entity.ts @@ -0,0 +1 @@ +export class Movie {} diff --git a/apps/api/src/movies/interfaces/search.interface.ts b/apps/api/src/movies/interfaces/search.interface.ts new file mode 100644 index 0000000..809b628 --- /dev/null +++ b/apps/api/src/movies/interfaces/search.interface.ts @@ -0,0 +1,3 @@ +export interface MovieSearch { + byTitle: (title: string) => Promise +} diff --git a/apps/api/src/movies/movies.controller.spec.ts b/apps/api/src/movies/movies.controller.spec.ts new file mode 100644 index 0000000..85529d8 --- /dev/null +++ b/apps/api/src/movies/movies.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { MoviesController } from './movies.controller' +import { MoviesService } from './movies.service' + +describe('MoviesController', () => { + let controller: MoviesController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MoviesController], + providers: [MoviesService] + }).compile() + + controller = module.get(MoviesController) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) +}) diff --git a/apps/api/src/movies/movies.controller.ts b/apps/api/src/movies/movies.controller.ts new file mode 100644 index 0000000..d53406f --- /dev/null +++ b/apps/api/src/movies/movies.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common' +import { MoviesService } from './movies.service' +import { CreateMovieDto } from './dto/create-movie.dto' +import { UpdateMovieDto } from './dto/update-movie.dto' + +@Controller('movies') +export class MoviesController { + constructor (private readonly moviesService: MoviesService) {} + + @Post() + create (@Body() createMovieDto: CreateMovieDto) { + return this.moviesService.create(createMovieDto) + } + + @Get() + findAll () { + return this.moviesService.findAll() + } + + @Get(':id') + findOne (@Param('id') id: string) { + return this.moviesService.findOne(+id) + } + + @Patch(':id') + update (@Param('id') id: string, @Body() updateMovieDto: UpdateMovieDto) { + return this.moviesService.update(+id, updateMovieDto) + } + + @Delete(':id') + remove (@Param('id') id: string) { + return this.moviesService.remove(+id) + } +} diff --git a/apps/api/src/movies/movies.module.ts b/apps/api/src/movies/movies.module.ts new file mode 100644 index 0000000..0883895 --- /dev/null +++ b/apps/api/src/movies/movies.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { MoviesService } from './movies.service' +import { MoviesController } from './movies.controller' + +@Module({ + controllers: [MoviesController], + providers: [MoviesService] +}) +export class MoviesModule {} diff --git a/apps/api/src/movies/movies.service.spec.ts b/apps/api/src/movies/movies.service.spec.ts new file mode 100644 index 0000000..15c1f35 --- /dev/null +++ b/apps/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('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/movies/movies.service.ts b/apps/api/src/movies/movies.service.ts new file mode 100644 index 0000000..e19e8ad --- /dev/null +++ b/apps/api/src/movies/movies.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common' +import { CreateMovieDto } from './dto/create-movie.dto' +import { UpdateMovieDto } from './dto/update-movie.dto' + +@Injectable() +export class MoviesService { + create (createMovieDto: CreateMovieDto) { + return 'This action adds a new movie' + } + + findAll () { + return 'This action returns all movies' + } + + findOne (id: number) { + return `This action returns a #${id} movie` + } + + update (id: number, updateMovieDto: UpdateMovieDto) { + return `This action updates a #${id} movie` + } + + remove (id: number) { + return `This action removes a #${id} movie` + } +} From da2419383d87e524a33bf2a7e3d03005d15e0bfa Mon Sep 17 00:00:00 2001 From: "R. A. Souza" Date: Sun, 7 Apr 2024 16:49:24 +0200 Subject: [PATCH 2/7] feat(api): :sparkles: implement search movie by title using Streaming Availability API --- apps/api/.env.example | 5 ++++ apps/api/.gitignore | 2 ++ apps/api/package.json | 2 ++ apps/api/src/app/app.module.ts | 3 ++- apps/api/src/database/schema.prisma | 6 +++++ .../movies/clients/streaming-api.module.ts | 24 +++++++++++++++++++ .../movies/clients/streaming-api.service.ts | 5 ++++ apps/api/src/movies/dto/update-movie.dto.ts | 4 ++-- .../interfaces/movies-search.interface.ts | 3 +++ .../src/movies/interfaces/search.interface.ts | 3 --- .../movies/movies-search.api.service.spec.ts | 18 ++++++++++++++ .../src/movies/movies-search.api.service.ts | 15 ++++++++++++ .../src/movies/movies-search.service.spec.ts | 18 ++++++++++++++ apps/api/src/movies/movies-search.service.ts | 9 +++++++ apps/api/src/movies/movies.controller.ts | 14 +++++++++-- apps/api/src/movies/movies.module.ts | 14 ++++++++++- package-lock.json | 15 +++++++++--- 17 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 apps/api/src/movies/clients/streaming-api.module.ts create mode 100644 apps/api/src/movies/clients/streaming-api.service.ts create mode 100644 apps/api/src/movies/interfaces/movies-search.interface.ts delete mode 100644 apps/api/src/movies/interfaces/search.interface.ts create mode 100644 apps/api/src/movies/movies-search.api.service.spec.ts create mode 100644 apps/api/src/movies/movies-search.api.service.ts create mode 100644 apps/api/src/movies/movies-search.service.spec.ts create mode 100644 apps/api/src/movies/movies-search.service.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index 9bd92e8..ceaea2a 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -5,3 +5,8 @@ # See the documentation for all the connection string options: https://pris.ly/d/connection-strings DATABASE_URL="postgresql://api:admin@localhost:5432/postgres?schema=public" + +RAPID_API_KEY="" + +STREAMING_AVAILABILITY_API_HOST="https://streaming-availability.p.rapidapi.com" +IMDB_API_HOST="https://imdb188.p.rapidapi.com" diff --git a/apps/api/.gitignore b/apps/api/.gitignore index 11ddd8d..92ce615 100644 --- a/apps/api/.gitignore +++ b/apps/api/.gitignore @@ -1,3 +1,5 @@ node_modules # Keep environment variables out of version control .env + +.nestjs_repl_history diff --git a/apps/api/package.json b/apps/api/package.json index b4c52fb..faa28e5 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -22,12 +22,14 @@ "console": "npm run start -- --watch --entryFile repl" }, "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.1", "@prisma/client": "^5.11.0", + "axios": "^1.6.8", "nest-winston": "^1.9.4", "nestjs-zod": "^3.0.0", "reflect-metadata": "^0.2.0", diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 3da82c1..a53d606 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -6,9 +6,10 @@ import { ZodValidationPipe } from 'nestjs-zod' import { AppController } from './app.controller' import { AppService } from './app.service' import { DatabaseModule } from '../database/database.module' +import { MoviesModule } from '../movies/movies.module' @Module({ - imports: [ConfigModule.forRoot(), DatabaseModule], + imports: [ConfigModule.forRoot({ isGlobal: true }), DatabaseModule, MoviesModule], controllers: [AppController], providers: [ Logger, diff --git a/apps/api/src/database/schema.prisma b/apps/api/src/database/schema.prisma index ee282c7..b3d0f1a 100644 --- a/apps/api/src/database/schema.prisma +++ b/apps/api/src/database/schema.prisma @@ -12,3 +12,9 @@ datasource db { provider = "postgresql" url = env("DATABASE_URL") } + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} diff --git a/apps/api/src/movies/clients/streaming-api.module.ts b/apps/api/src/movies/clients/streaming-api.module.ts new file mode 100644 index 0000000..637c2f2 --- /dev/null +++ b/apps/api/src/movies/clients/streaming-api.module.ts @@ -0,0 +1,24 @@ +import { HttpModule, HttpService } from '@nestjs/axios' +import { Module } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { StreamingApiHttpService } from './streaming-api.service' + +@Module({ + imports: [HttpModule.registerAsync({ + useFactory: async (config: ConfigService) => ({ + baseURL: config.get('STREAMING_AVAILABILITY_API_HOST'), + headers: { + 'X-RapidAPI-Key': config.get('RAPID_API_KEY') + } + }), + inject: [ConfigService] + })], + providers: [ + { + provide: StreamingApiHttpService, + useExisting: HttpService + } + ], + exports: [StreamingApiHttpService] +}) +export class StreamingApiModule {} diff --git a/apps/api/src/movies/clients/streaming-api.service.ts b/apps/api/src/movies/clients/streaming-api.service.ts new file mode 100644 index 0000000..2f7302f --- /dev/null +++ b/apps/api/src/movies/clients/streaming-api.service.ts @@ -0,0 +1,5 @@ +import { HttpService } from '@nestjs/axios' +import { Injectable } from '@nestjs/common' + +@Injectable() +export abstract class StreamingApiHttpService extends HttpService {} diff --git a/apps/api/src/movies/dto/update-movie.dto.ts b/apps/api/src/movies/dto/update-movie.dto.ts index 7efc78d..432457b 100644 --- a/apps/api/src/movies/dto/update-movie.dto.ts +++ b/apps/api/src/movies/dto/update-movie.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateMovieDto } from './create-movie.dto'; +import { PartialType } from '@nestjs/swagger' +import { CreateMovieDto } from './create-movie.dto' export class UpdateMovieDto extends PartialType(CreateMovieDto) {} diff --git a/apps/api/src/movies/interfaces/movies-search.interface.ts b/apps/api/src/movies/interfaces/movies-search.interface.ts new file mode 100644 index 0000000..9bb9cb7 --- /dev/null +++ b/apps/api/src/movies/interfaces/movies-search.interface.ts @@ -0,0 +1,3 @@ +export interface MoviesSearch { + findByTitle: (title: string) => Promise +} diff --git a/apps/api/src/movies/interfaces/search.interface.ts b/apps/api/src/movies/interfaces/search.interface.ts deleted file mode 100644 index 809b628..0000000 --- a/apps/api/src/movies/interfaces/search.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface MovieSearch { - byTitle: (title: string) => Promise -} diff --git a/apps/api/src/movies/movies-search.api.service.spec.ts b/apps/api/src/movies/movies-search.api.service.spec.ts new file mode 100644 index 0000000..1089694 --- /dev/null +++ b/apps/api/src/movies/movies-search.api.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { MoviesSearchApiService } from './movies-search.api.service' + +describe('MoviesSearchApiService', () => { + let service: MoviesSearchApiService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MoviesSearchApiService] + }).compile() + + service = module.get(MoviesSearchApiService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/movies/movies-search.api.service.ts b/apps/api/src/movies/movies-search.api.service.ts new file mode 100644 index 0000000..c38c230 --- /dev/null +++ b/apps/api/src/movies/movies-search.api.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common' + +import { firstValueFrom } from 'rxjs' +import { StreamingApiHttpService } from './clients/streaming-api.service' +import { MoviesSearch } from './interfaces/movies-search.interface' + +@Injectable() +export class MoviesSearchApiService implements MoviesSearch { + constructor (private readonly streamingClient: StreamingApiHttpService) {} + + async findByTitle (title: string) { + const { data } = await firstValueFrom(this.streamingClient.get('/search/title', { params: { title, country: 'se' } })) + return data + } +} diff --git a/apps/api/src/movies/movies-search.service.spec.ts b/apps/api/src/movies/movies-search.service.spec.ts new file mode 100644 index 0000000..90fb878 --- /dev/null +++ b/apps/api/src/movies/movies-search.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { MoviesSearchService } from './movies-search.service' + +describe('MoviesSearchService', () => { + let service: MoviesSearchService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MoviesSearchService] + }).compile() + + service = module.get(MoviesSearchService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/movies/movies-search.service.ts b/apps/api/src/movies/movies-search.service.ts new file mode 100644 index 0000000..30f4ebe --- /dev/null +++ b/apps/api/src/movies/movies-search.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common' +import { MoviesSearch } from './interfaces/movies-search.interface' + +// Only used for NestJS to find the correct provider to inject into MoviesController + +@Injectable() +export abstract class MoviesSearchService implements MoviesSearch { + abstract findByTitle (title: string): Promise +} diff --git a/apps/api/src/movies/movies.controller.ts b/apps/api/src/movies/movies.controller.ts index d53406f..f181301 100644 --- a/apps/api/src/movies/movies.controller.ts +++ b/apps/api/src/movies/movies.controller.ts @@ -1,11 +1,21 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common' +import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common' import { MoviesService } from './movies.service' import { CreateMovieDto } from './dto/create-movie.dto' import { UpdateMovieDto } from './dto/update-movie.dto' +import { MoviesSearchService } from './movies-search.service' @Controller('movies') export class MoviesController { - constructor (private readonly moviesService: MoviesService) {} + constructor (private readonly moviesService: MoviesService, private readonly movieSearchService: MoviesSearchService) {} + + @Get('search') + async findByTitle (@Query('title') title: string) { + return await this.movieSearchService.findByTitle(title) + } + + /** + * RESTful API endpoints + */ @Post() create (@Body() createMovieDto: CreateMovieDto) { diff --git a/apps/api/src/movies/movies.module.ts b/apps/api/src/movies/movies.module.ts index 0883895..23f566f 100644 --- a/apps/api/src/movies/movies.module.ts +++ b/apps/api/src/movies/movies.module.ts @@ -1,9 +1,21 @@ import { Module } from '@nestjs/common' import { MoviesService } from './movies.service' import { MoviesController } from './movies.controller' +import { StreamingApiModule } from './clients/streaming-api.module' +import { MoviesSearchApiService } from './movies-search.api.service' +import { MoviesSearchService } from './movies-search.service' @Module({ + imports: [StreamingApiModule], controllers: [MoviesController], - providers: [MoviesService] + providers: [ + MoviesService, + + // TODO: switch to MoviesSearchDbService as soon as the scraper is implemented + { + provide: MoviesSearchService, + useClass: MoviesSearchApiService + } + ] }) export class MoviesModule {} diff --git a/package-lock.json b/package-lock.json index e7a007b..518bbb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,12 +27,14 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.1", "@prisma/client": "^5.11.0", + "axios": "^1.6.8", "nest-winston": "^1.9.4", "nestjs-zod": "^3.0.0", "reflect-metadata": "^0.2.0", @@ -5437,6 +5439,16 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/@nestjs/axios": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", + "integrity": "sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", @@ -10158,7 +10170,6 @@ "version": "1.6.8", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dev": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -15129,7 +15140,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -15298,7 +15308,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", From df9e8ecef89a082048651a6b92b6026b8efaf279 Mon Sep 17 00:00:00 2001 From: "R. A. Souza" Date: Sun, 7 Apr 2024 17:04:31 +0200 Subject: [PATCH 3/7] refactor(api): :recycle: migrate movie search to IMDB API --- .../api/src/movies/clients/imdb-api.module.ts | 24 +++++++++++++++++++ .../src/movies/clients/imdb-api.service.ts | 5 ++++ apps/api/src/movies/dto/create-movie.dto.ts | 1 + apps/api/src/movies/entities/movie.entity.ts | 1 + .../interfaces/movies-search.interface.ts | 2 ++ .../src/movies/movies-search.api.service.ts | 6 ++--- apps/api/src/movies/movies-search.service.ts | 2 ++ apps/api/src/movies/movies.module.ts | 3 ++- 8 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 apps/api/src/movies/clients/imdb-api.module.ts create mode 100644 apps/api/src/movies/clients/imdb-api.service.ts diff --git a/apps/api/src/movies/clients/imdb-api.module.ts b/apps/api/src/movies/clients/imdb-api.module.ts new file mode 100644 index 0000000..da7a7cd --- /dev/null +++ b/apps/api/src/movies/clients/imdb-api.module.ts @@ -0,0 +1,24 @@ +import { HttpModule, HttpService } from '@nestjs/axios' +import { Module } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { ImdbApiHttpService } from './imdb-api.service' + +@Module({ + imports: [HttpModule.registerAsync({ + useFactory: async (config: ConfigService) => ({ + baseURL: config.get('IMDB_API_HOST'), + headers: { + 'X-RapidAPI-Key': config.get('RAPID_API_KEY') + } + }), + inject: [ConfigService] + })], + providers: [ + { + provide: ImdbApiHttpService, + useExisting: HttpService + } + ], + exports: [ImdbApiHttpService] +}) +export class ImdbApiModule {} diff --git a/apps/api/src/movies/clients/imdb-api.service.ts b/apps/api/src/movies/clients/imdb-api.service.ts new file mode 100644 index 0000000..03b0a43 --- /dev/null +++ b/apps/api/src/movies/clients/imdb-api.service.ts @@ -0,0 +1,5 @@ +import { HttpService } from '@nestjs/axios' +import { Injectable } from '@nestjs/common' + +@Injectable() +export abstract class ImdbApiHttpService extends HttpService {} diff --git a/apps/api/src/movies/dto/create-movie.dto.ts b/apps/api/src/movies/dto/create-movie.dto.ts index 4124da8..5306a34 100644 --- a/apps/api/src/movies/dto/create-movie.dto.ts +++ b/apps/api/src/movies/dto/create-movie.dto.ts @@ -1 +1,2 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ export class CreateMovieDto {} diff --git a/apps/api/src/movies/entities/movie.entity.ts b/apps/api/src/movies/entities/movie.entity.ts index 716ece6..aa4d995 100644 --- a/apps/api/src/movies/entities/movie.entity.ts +++ b/apps/api/src/movies/entities/movie.entity.ts @@ -1 +1,2 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ export class Movie {} diff --git a/apps/api/src/movies/interfaces/movies-search.interface.ts b/apps/api/src/movies/interfaces/movies-search.interface.ts index 9bb9cb7..ea7d9fe 100644 --- a/apps/api/src/movies/interfaces/movies-search.interface.ts +++ b/apps/api/src/movies/interfaces/movies-search.interface.ts @@ -1,3 +1,5 @@ export interface MoviesSearch { + // TODO: Implement Movie response interface + // eslint-disable-next-line @typescript-eslint/no-explicit-any findByTitle: (title: string) => Promise } diff --git a/apps/api/src/movies/movies-search.api.service.ts b/apps/api/src/movies/movies-search.api.service.ts index c38c230..d837009 100644 --- a/apps/api/src/movies/movies-search.api.service.ts +++ b/apps/api/src/movies/movies-search.api.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@nestjs/common' import { firstValueFrom } from 'rxjs' -import { StreamingApiHttpService } from './clients/streaming-api.service' import { MoviesSearch } from './interfaces/movies-search.interface' +import { ImdbApiHttpService } from './clients/imdb-api.service' @Injectable() export class MoviesSearchApiService implements MoviesSearch { - constructor (private readonly streamingClient: StreamingApiHttpService) {} + constructor (private readonly imdbClient: ImdbApiHttpService) {} async findByTitle (title: string) { - const { data } = await firstValueFrom(this.streamingClient.get('/search/title', { params: { title, country: 'se' } })) + const { data } = await firstValueFrom(this.imdbClient.get('/api/v1/searchIMDB', { params: { query: title } })) return data } } diff --git a/apps/api/src/movies/movies-search.service.ts b/apps/api/src/movies/movies-search.service.ts index 30f4ebe..a6ea9c7 100644 --- a/apps/api/src/movies/movies-search.service.ts +++ b/apps/api/src/movies/movies-search.service.ts @@ -5,5 +5,7 @@ import { MoviesSearch } from './interfaces/movies-search.interface' @Injectable() export abstract class MoviesSearchService implements MoviesSearch { + // TODO: Implement Movie response interface + // eslint-disable-next-line @typescript-eslint/no-explicit-any abstract findByTitle (title: string): Promise } diff --git a/apps/api/src/movies/movies.module.ts b/apps/api/src/movies/movies.module.ts index 23f566f..df29f23 100644 --- a/apps/api/src/movies/movies.module.ts +++ b/apps/api/src/movies/movies.module.ts @@ -4,9 +4,10 @@ import { MoviesController } from './movies.controller' import { StreamingApiModule } from './clients/streaming-api.module' import { MoviesSearchApiService } from './movies-search.api.service' import { MoviesSearchService } from './movies-search.service' +import { ImdbApiModule } from './clients/imdb-api.module' @Module({ - imports: [StreamingApiModule], + imports: [StreamingApiModule, ImdbApiModule], controllers: [MoviesController], providers: [ MoviesService, From 5e4e618a8493bb108f959c1eabbeec5894a9b5af Mon Sep 17 00:00:00 2001 From: rasouza Date: Mon, 8 Apr 2024 19:34:54 +0200 Subject: [PATCH 4/7] refactor(api): :recycle: refactor API clients to a separate service builder --- apps/api/src/movies/clients/imdb-api.module.ts | 11 ++--------- apps/api/src/movies/clients/imdb-api.service.ts | 17 ++++++++++++++++- .../src/movies/clients/streaming-api.module.ts | 11 ++--------- .../src/movies/clients/streaming-api.service.ts | 17 ++++++++++++++++- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/apps/api/src/movies/clients/imdb-api.module.ts b/apps/api/src/movies/clients/imdb-api.module.ts index da7a7cd..84e81b9 100644 --- a/apps/api/src/movies/clients/imdb-api.module.ts +++ b/apps/api/src/movies/clients/imdb-api.module.ts @@ -1,17 +1,10 @@ import { HttpModule, HttpService } from '@nestjs/axios' import { Module } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { ImdbApiHttpService } from './imdb-api.service' +import { ImdbApiBuilder, ImdbApiHttpService } from './imdb-api.service' @Module({ imports: [HttpModule.registerAsync({ - useFactory: async (config: ConfigService) => ({ - baseURL: config.get('IMDB_API_HOST'), - headers: { - 'X-RapidAPI-Key': config.get('RAPID_API_KEY') - } - }), - inject: [ConfigService] + useClass: ImdbApiBuilder })], providers: [ { diff --git a/apps/api/src/movies/clients/imdb-api.service.ts b/apps/api/src/movies/clients/imdb-api.service.ts index 03b0a43..39e85fd 100644 --- a/apps/api/src/movies/clients/imdb-api.service.ts +++ b/apps/api/src/movies/clients/imdb-api.service.ts @@ -1,5 +1,20 @@ -import { HttpService } from '@nestjs/axios' +import { HttpModuleOptions, HttpModuleOptionsFactory, HttpService } from '@nestjs/axios' import { Injectable } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' @Injectable() +export class ImdbApiBuilder implements HttpModuleOptionsFactory { + constructor (private readonly config: ConfigService) { + } + + createHttpOptions (): HttpModuleOptions { + return { + baseURL: this.config.get('IMDB_API_HOST'), + headers: { + 'X-RapidAPI-Key': this.config.get('RAPID_API_KEY') + } + } + } +} + export abstract class ImdbApiHttpService extends HttpService {} diff --git a/apps/api/src/movies/clients/streaming-api.module.ts b/apps/api/src/movies/clients/streaming-api.module.ts index 637c2f2..5bbc905 100644 --- a/apps/api/src/movies/clients/streaming-api.module.ts +++ b/apps/api/src/movies/clients/streaming-api.module.ts @@ -1,17 +1,10 @@ import { HttpModule, HttpService } from '@nestjs/axios' import { Module } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { StreamingApiHttpService } from './streaming-api.service' +import { StreamingApiBuilder, StreamingApiHttpService } from './streaming-api.service' @Module({ imports: [HttpModule.registerAsync({ - useFactory: async (config: ConfigService) => ({ - baseURL: config.get('STREAMING_AVAILABILITY_API_HOST'), - headers: { - 'X-RapidAPI-Key': config.get('RAPID_API_KEY') - } - }), - inject: [ConfigService] + useClass: StreamingApiBuilder })], providers: [ { diff --git a/apps/api/src/movies/clients/streaming-api.service.ts b/apps/api/src/movies/clients/streaming-api.service.ts index 2f7302f..adee84b 100644 --- a/apps/api/src/movies/clients/streaming-api.service.ts +++ b/apps/api/src/movies/clients/streaming-api.service.ts @@ -1,5 +1,20 @@ -import { HttpService } from '@nestjs/axios' +import { HttpModuleOptions, HttpModuleOptionsFactory, HttpService } from '@nestjs/axios' import { Injectable } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' @Injectable() +export class StreamingApiBuilder implements HttpModuleOptionsFactory { + constructor (private readonly config: ConfigService) { + } + + createHttpOptions (): HttpModuleOptions { + return { + baseURL: this.config.get('STREAMING_AVAILABILITY_API_HOST'), + headers: { + 'X-RapidAPI-Key': this.config.get('RAPID_API_KEY') + } + } + } +} + export abstract class StreamingApiHttpService extends HttpService {} From 2feabc0c2ce582e604e6b33ff4330f497f323ce8 Mon Sep 17 00:00:00 2001 From: rasouza Date: Mon, 8 Apr 2024 19:37:03 +0200 Subject: [PATCH 5/7] feat(api): :technologist: validate config before server bootstrap --- apps/api/src/app/app.module.ts | 3 ++- apps/api/src/app/config/validate.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/app/config/validate.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index a53d606..36ab258 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -7,9 +7,10 @@ import { AppController } from './app.controller' import { AppService } from './app.service' import { DatabaseModule } from '../database/database.module' import { MoviesModule } from '../movies/movies.module' +import { validate } from './config/validate' @Module({ - imports: [ConfigModule.forRoot({ isGlobal: true }), DatabaseModule, MoviesModule], + imports: [ConfigModule.forRoot({ isGlobal: true, validate }), DatabaseModule, MoviesModule], controllers: [AppController], providers: [ Logger, diff --git a/apps/api/src/app/config/validate.ts b/apps/api/src/app/config/validate.ts new file mode 100644 index 0000000..433a275 --- /dev/null +++ b/apps/api/src/app/config/validate.ts @@ -0,0 +1,15 @@ +import { z } from 'nestjs-zod/z' + +export function validate (config: Record) { + const schema = z.object({ + DATABASE_URL: z.string().url(), + STREAMING_AVAILABILITY_API_HOST: z.string().url().optional(), + IMDB_API_HOST: z.string().url().optional(), + RAPID_API_KEY: z.string().optional() + + }) + + schema.parse(config) + + return config +} From dd9443b16ff730f1cb31cfda861b9600f14537db Mon Sep 17 00:00:00 2001 From: rasouza Date: Tue, 9 Apr 2024 18:05:00 +0200 Subject: [PATCH 6/7] feat(api): :wrench: update vite config to accept incoming requests from mobile --- apps/api/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/vite.config.ts b/apps/api/vite.config.ts index d9398f0..578fb55 100644 --- a/apps/api/vite.config.ts +++ b/apps/api/vite.config.ts @@ -7,6 +7,7 @@ export default defineConfig(({ command, mode }: ConfigEnv) => { base: '/api', server: { // vite server configs, for details see \[vite doc\](https://vitejs.dev/config/#server-host) + host: '0.0.0.0', port: 3000 }, define: { From d1d470f9236935959a0e971c4c750dc8da27b415 Mon Sep 17 00:00:00 2001 From: rasouza Date: Tue, 9 Apr 2024 20:23:10 +0200 Subject: [PATCH 7/7] test(api): :white_check_mark: add missing test for movie search --- .../movies/interfaces/imdb-api.interface.ts | 16 +++++ .../movies/movies-search.api.service.spec.ts | 59 ++++++++++++++++++- .../src/movies/movies-search.api.service.ts | 6 +- apps/api/src/movies/movies.controller.spec.ts | 44 +++++++++++++- apps/api/src/movies/movies.controller.ts | 4 +- 5 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/movies/interfaces/imdb-api.interface.ts diff --git a/apps/api/src/movies/interfaces/imdb-api.interface.ts b/apps/api/src/movies/interfaces/imdb-api.interface.ts new file mode 100644 index 0000000..6c637ff --- /dev/null +++ b/apps/api/src/movies/interfaces/imdb-api.interface.ts @@ -0,0 +1,16 @@ +export interface IMDBSearchResponse { + status: boolean + message: string + timestamp: number + data: IMDBMovie[] +} + +export interface IMDBMovie { + id: string + qid: string + title: string + year: number + stars: string + q: string + image: string +} diff --git a/apps/api/src/movies/movies-search.api.service.spec.ts b/apps/api/src/movies/movies-search.api.service.spec.ts index 1089694..059115e 100644 --- a/apps/api/src/movies/movies-search.api.service.spec.ts +++ b/apps/api/src/movies/movies-search.api.service.spec.ts @@ -1,18 +1,73 @@ import { Test, TestingModule } from '@nestjs/testing' import { MoviesSearchApiService } from './movies-search.api.service' +import { ImdbApiHttpService } from './clients/imdb-api.service' +import { HttpService } from '@nestjs/axios' +import { createMock } from '@golevelup/ts-jest' +import { IMDBSearchResponse } from './interfaces/imdb-api.interface' +import { AxiosResponse } from 'axios' +import { of } from 'rxjs' describe('MoviesSearchApiService', () => { let service: MoviesSearchApiService + let httpService: ImdbApiHttpService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [MoviesSearchApiService] - }).compile() + providers: [ + MoviesSearchApiService, + { + provide: ImdbApiHttpService, + useExisting: HttpService + } + ] + }).useMocker(createMock).compile() service = module.get(MoviesSearchApiService) + httpService = module.get(ImdbApiHttpService) }) it('should be defined', () => { expect(service).toBeDefined() }) + + it('should return all movies with given title', async () => { + const data: IMDBSearchResponse = { + status: true, + message: 'Success', + timestamp: 1689187551887, + data: [ + { + id: 'tt9603212', + qid: 'movie', + title: 'Mission: Impossible - Dead Reckoning Part One', + year: 2023, + stars: 'Tom Cruise, Hayley Atwell', + q: 'feature', + image: 'https://m.media-amazon.com/images/M/MV5BY2VmZDhhNjgtNDcxYi00M2I3LThlMTQtMWRiNWI2Y2I4ZjRmXkEyXkFqcGdeQXVyMTMxMTIwMTE0._V1_.jpg' + }, + { + id: 'tt0117060', + qid: 'movie', + title: 'Mission: Impossible', + year: 1996, + stars: 'Tom Cruise, Jon Voight', + q: 'feature', + image: 'https://m.media-amazon.com/images/M/MV5BMTc3NjI2MjU0Nl5BMl5BanBnXkFtZTgwNDk3ODYxMTE@._V1_.jpg' + } + ] + } + + const response: AxiosResponse = { + data, + status: 200, + statusText: 'OK', + headers: {}, + config: { url: 'http://localhost:3000/mockUrl' } + } + + jest.spyOn(httpService, 'get').mockReturnValue(of(response)) + + const movies = await service.findByTitle('Mission') + expect(movies).toEqual(data.data) + }) }) diff --git a/apps/api/src/movies/movies-search.api.service.ts b/apps/api/src/movies/movies-search.api.service.ts index d837009..f1473da 100644 --- a/apps/api/src/movies/movies-search.api.service.ts +++ b/apps/api/src/movies/movies-search.api.service.ts @@ -3,13 +3,15 @@ import { Injectable } from '@nestjs/common' import { firstValueFrom } from 'rxjs' import { MoviesSearch } from './interfaces/movies-search.interface' import { ImdbApiHttpService } from './clients/imdb-api.service' +import { IMDBMovie } from './interfaces/imdb-api.interface' @Injectable() export class MoviesSearchApiService implements MoviesSearch { constructor (private readonly imdbClient: ImdbApiHttpService) {} - async findByTitle (title: string) { + async findByTitle (title: string): Promise { const { data } = await firstValueFrom(this.imdbClient.get('/api/v1/searchIMDB', { params: { query: title } })) - return data + const { data: movies } = data + return movies } } diff --git a/apps/api/src/movies/movies.controller.spec.ts b/apps/api/src/movies/movies.controller.spec.ts index 85529d8..2ed1d4e 100644 --- a/apps/api/src/movies/movies.controller.spec.ts +++ b/apps/api/src/movies/movies.controller.spec.ts @@ -1,20 +1,60 @@ import { Test, TestingModule } from '@nestjs/testing' import { MoviesController } from './movies.controller' import { MoviesService } from './movies.service' +import { MoviesSearchService } from './movies-search.service' +import { createMock } from '@golevelup/ts-jest' +import { MoviesSearchApiService } from './movies-search.api.service' + +const movies = [ + { + id: 'tt9603212', + qid: 'movie', + title: 'Mission: Impossible - Dead Reckoning Part One', + year: 2023, + stars: 'Tom Cruise, Hayley Atwell', + q: 'feature', + image: 'https://m.media-amazon.com/images/M/MV5BY2VmZDhhNjgtNDcxYi00M2I3LThlMTQtMWRiNWI2Y2I4ZjRmXkEyXkFqcGdeQXVyMTMxMTIwMTE0._V1_.jpg' + }, + { + id: 'tt0117060', + qid: 'movie', + title: 'Mission: Impossible', + year: 1996, + stars: 'Tom Cruise, Jon Voight', + q: 'feature', + image: 'https://m.media-amazon.com/images/M/MV5BMTc3NjI2MjU0Nl5BMl5BanBnXkFtZTgwNDk3ODYxMTE@._V1_.jpg' + } +] describe('MoviesController', () => { let controller: MoviesController + let movieSearchService: MoviesSearchService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [MoviesController], - providers: [MoviesService] - }).compile() + providers: [ + MoviesService, + { + provide: MoviesSearchService, + useClass: MoviesSearchApiService + } + ] + }).useMocker(createMock).compile() controller = module.get(MoviesController) + movieSearchService = module.get(MoviesSearchService) }) it('should be defined', () => { expect(controller).toBeDefined() }) + + it('should find a movie by title', async () => { + movieSearchService.findByTitle = jest.fn().mockResolvedValueOnce(movies) + + const response = await controller.findByTitle('Mission: Impossible') + + expect(response).toEqual(movies) + }) }) diff --git a/apps/api/src/movies/movies.controller.ts b/apps/api/src/movies/movies.controller.ts index f181301..5908b54 100644 --- a/apps/api/src/movies/movies.controller.ts +++ b/apps/api/src/movies/movies.controller.ts @@ -9,7 +9,9 @@ export class MoviesController { constructor (private readonly moviesService: MoviesService, private readonly movieSearchService: MoviesSearchService) {} @Get('search') - async findByTitle (@Query('title') title: string) { + // TODO: change return type to Movie[] when the entity is defined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async findByTitle (@Query('title') title: string): Promise { return await this.movieSearchService.findByTitle(title) }