implemented movies controller

main
Inga 🏳‍🌈 12 months ago
parent 5147882e6a
commit f647b8e7fb
  1. 35
      package-lock.json
  2. 2
      package.json
  3. 22
      src/app.controller.spec.ts
  4. 12
      src/app.controller.ts
  5. 93
      src/app.module.spec.ts
  6. 13
      src/app.module.ts
  7. 8
      src/app.service.ts
  8. 2
      src/main.ts
  9. 48
      src/movies.controller.spec.ts
  10. 102
      src/movies.controller.ts

35
package-lock.json generated

@ -12,6 +12,8 @@
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"p-queue": "^6.6.2", "p-queue": "^6.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@ -2121,6 +2123,11 @@
"@types/superagent": "*" "@types/superagent": "*"
} }
}, },
"node_modules/@types/validator": {
"version": "13.11.7",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.7.tgz",
"integrity": "sha512-q0JomTsJ2I5Mv7dhHhQLGjMvX0JJm5dyZ1DXQySIUzU1UlwzB8bt+R6+LODUbz0UDIOvEzGc28tk27gBJw2N8Q=="
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.32", "version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
@ -3203,6 +3210,21 @@
"integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==",
"dev": true "dev": true
}, },
"node_modules/class-transformer": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
},
"node_modules/class-validator": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz",
"integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==",
"dependencies": {
"@types/validator": "^13.7.10",
"libphonenumber-js": "^1.10.14",
"validator": "^13.7.0"
}
},
"node_modules/cli-cursor": { "node_modules/cli-cursor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@ -6160,6 +6182,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/libphonenumber-js": {
"version": "1.10.51",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.51.tgz",
"integrity": "sha512-vY2I+rQwrDQzoPds0JeTEpeWzbUJgqoV0O4v31PauHBb/e+1KCXKylHcDnBMgJZ9fH9mErsEbROJY3Z3JtqEmg=="
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -8557,6 +8584,14 @@
"node": ">=10.12.0" "node": ">=10.12.0"
} }
}, },
"node_modules/validator": {
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
"integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

@ -23,6 +23,8 @@
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"p-queue": "^6.6.2", "p-queue": "^6.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",

@ -1,22 +0,0 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

@ -0,0 +1,93 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ValidationPipe, type INestApplication } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from './../src/app.module';
import { createTestOmdbEnrichedDataService } from './utils/testHelpers';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const enrichedDataService = await createTestOmdbEnrichedDataService();
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider('enrichedDataService')
.useValue(enrichedDataService)
.compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.init();
});
it('/api/movies/tt0061852 (GET)', () => {
return request(app.getHttpServer())
.get('/api/movies/tt0061852')
.expect(200)
.expect((response) => {
expect(response.body).toMatchObject({
title: 'The Jungle Book',
});
});
});
it('/api/movies/11528860 (GET)', () => {
return request(app.getHttpServer())
.get('/api/movies/11528860')
.expect(200)
.expect((response) => {
expect(response.body).toMatchObject({
title: 'The Jungle Book',
});
});
});
it('/api/movies (GET)', () => {
return request(app.getHttpServer()).get('/api/movies').expect(400);
});
it('/api/movies?writers=George Lucas (GET)', () => {
return request(app.getHttpServer())
.get('/api/movies?writers=George Lucas')
.expect(200)
.expect((response) => {
expect(response.body).toMatchObject([
{ title: 'Indiana Jones and the Last Crusade' },
{ title: 'Star Wars: Episode IV - A New Hope' },
]);
});
});
it('/api/movies?writers=George Lucas&actors=Mark Hamill (GET)', () => {
return request(app.getHttpServer())
.get('/api/movies?writers=George Lucas&actors=Mark Hamill')
.expect(200)
.expect((response) => {
expect(response.body).toMatchObject([
{ title: 'Star Wars: Episode IV - A New Hope' },
]);
});
});
it('/api/movies?productionYear=1967 (GET)', () => {
return request(app.getHttpServer())
.get('/api/movies?productionYear=1967')
.expect(200)
.expect((response) => {
expect(response.body).toMatchObject([
{ title: 'The Jungle Book' },
]);
});
});
it('/api/movies?productionYear=1800 (GET)', () => {
return request(app.getHttpServer())
.get('/api/movies?productionYear=1800')
.expect(200)
.expect((response) => {
expect(response.body).toMatchObject([]);
});
});
});

@ -1,11 +1,16 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AppController } from './app.controller'; import { MoviesController } from './movies.controller';
import { AppService } from './app.service'; import { createTestOmdbEnrichedDataService } from './utils/testHelpers';
@Module({ @Module({
imports: [], imports: [],
controllers: [AppController], controllers: [MoviesController],
providers: [AppService], providers: [
{
provide: 'enrichedDataService',
useFactory: createTestOmdbEnrichedDataService,
},
],
}) })
// eslint-disable-next-line @typescript-eslint/no-extraneous-class // eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class AppModule {} export class AppModule {}

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

@ -1,8 +1,10 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.listen(3000); await app.listen(3000);
} }
void bootstrap(); void bootstrap();

@ -0,0 +1,48 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { MoviesController } from './movies.controller';
import { createTestOmdbEnrichedDataService } from './utils/testHelpers';
describe('AppController', () => {
let moviesController: MoviesController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [MoviesController],
providers: [
{
provide: 'enrichedDataService',
useFactory: createTestOmdbEnrichedDataService,
},
],
}).compile();
moviesController = app.get<MoviesController>(MoviesController);
});
describe('root', () => {
it('should return movie by internal id', () => {
expect(moviesController.getMovie('11528860')).toMatchObject({
title: 'The Jungle Book',
});
});
it('should return movie by imdb id', () => {
expect(moviesController.getMovie('tt0061852')).toMatchObject({
title: 'The Jungle Book',
});
});
it('should throw an error for empty search query', () => {
expect(() => moviesController.searchMovies({})).toThrow();
});
it('should return movies matching writer', () => {
expect(
moviesController.searchMovies({ writers: 'George Lucas' }),
).toMatchObject([
{ title: 'Indiana Jones and the Last Crusade' },
{ title: 'Star Wars: Episode IV - A New Hope' },
]);
});
});
});

@ -0,0 +1,102 @@
import {
BadRequestException,
Controller,
Get,
Inject,
NotFoundException,
Param,
Query,
} from '@nestjs/common';
import { EnrichedDataService } from './types';
import { IsIn, IsInt, IsOptional, IsString, Length } from 'class-validator';
import { Type } from 'class-transformer';
const ENTRY_TYPES = ['movie', 'series', 'episode'] as const;
class SearchDto {
@IsString()
@Length(1)
@IsOptional()
actors?: string;
@IsString()
@Length(2, 2)
@IsOptional()
availableLanguages?: string;
@IsString()
@Length(1)
@IsOptional()
directors?: string;
@IsString()
@Length(1)
@IsOptional()
localTitle?: string;
@IsString()
@Length(2, 2)
@IsOptional()
originalLanguage?: string;
@IsString()
@Length(1)
@IsOptional()
productionCountries?: string;
@IsInt()
@Type(() => Number)
@IsOptional()
productionYear?: number;
@IsString()
@Length(1)
@IsOptional()
studios?: string;
@IsString()
@Length(1)
@IsOptional()
title?: string;
@IsString()
@IsIn(ENTRY_TYPES)
@IsOptional()
type?: (typeof ENTRY_TYPES)[number];
@IsString()
@Length(1)
@IsOptional()
writers?: string;
}
@Controller('/api/movies')
export class MoviesController {
constructor(
@Inject('enrichedDataService')
private readonly enrichedDataService: EnrichedDataService,
) {}
@Get()
searchMovies(@Query() query: SearchDto) {
if (!Object.values(query).some((filterValue) => filterValue)) {
throw new BadRequestException('Query should not be empty');
}
const result = this.enrichedDataService.getSearchResults(query);
return result;
}
@Get(':id')
getMovie(@Param('id') id: string) {
const result = /^\d+$/.test(id)
? this.enrichedDataService.getMetadataByInternalId(parseInt(id, 10))
: this.enrichedDataService.getMetadataByImdbId(id);
if (!result) {
throw new NotFoundException();
}
return result;
}
}
Loading…
Cancel
Save