diff --git a/docker-compose.yaml b/docker-compose.yaml index 2f7d2f5..76842c4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -17,8 +17,10 @@ services: image: ${ES_IMAGE_NAME}:${ES_IMAGE_VERSION} build: context: . - dockerfile: Dockerfile container_name: ${ES_CONTAINER_NAME} + restart: always + ports: + - ${ES_PORT}:${ES_PORT} environment: - xpack.security.enabled=false - discovery.type=single-node diff --git a/package-lock.json b/package-lock.json index a40872b..a2ec462 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "@types/express": "^4.17.13", "@types/jest": "27.5.1", "@types/node": "^17.0.38", - "@types/supertest": "^2.0.11", + "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", diff --git a/package.json b/package.json index b89cb23..c49787b 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@types/express": "^4.17.13", "@types/jest": "27.5.1", "@types/node": "^17.0.38", - "@types/supertest": "^2.0.11", + "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", @@ -75,7 +75,8 @@ "json", "ts" ], - "rootDir": "src", + "modulePaths": [""], + "rootDir": "./", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 102b5d6..785a47c 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -1,22 +1,45 @@ -import { Controller, Get, Param, Put, Query, Res } from "@nestjs/common"; -import { SearchService } from "src/core/services/common/search.service"; -import { Response } from "express"; +import { Controller, Get, Next, Param, ParseUUIDPipe, Put, Query, Req, Res } from "@nestjs/common"; +import { EsResponseDto } from "src/core/domain/dtos"; +import { SearchService } from "../../core/services/common/search.service"; +import { response, Response } from "express"; @Controller('papers') export class PapersController { constructor(private searchService: SearchService) {} + /** + * + * @param query + * @param response + * @returns a response with a set of matching papers + */ @Get('search') - getByContext(@Query('query') query: string/*, @Query('page') page, @Query('limit') limit*/): object { - return this.searchService.findByContext(query); + getByContext(@Query('query') query: string, @Res() response: Response/*, @Query('page') page, @Query('limit') limit*/) { + return this.searchService.findByContext(query).then( + (res) => { + response.status(200).send(res); + }, + (err) => { + response.status(err).send(); + } + ); } - @Get(':id') - getByID(@Param('id') id: string): object { - if(!id) { - //response.status(400).send({msg: "fff"}); - } else { - return this.searchService.findByID(id); - } + /** + * + * @param uuid + * @param response + * @returns a response with a requested object + */ + @Get(':uuid') + getByID(@Param('uuid', ParseUUIDPipe) uuid: string, @Res() response: Response) { + return this.searchService.findByID(uuid).then( + (res) => { + response.status(200).send(res); + }, + (err) => { + response.status(err).send(); + } + ); } } \ No newline at end of file diff --git a/src/core/domain/dtos/es-response.dto.ts b/src/core/domain/dtos/es-response.dto.ts new file mode 100644 index 0000000..f7899b4 --- /dev/null +++ b/src/core/domain/dtos/es-response.dto.ts @@ -0,0 +1,83 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional } from "class-validator"; + +/** + * List of allowed properties in this DTO + */ +const allowedProperties = ['took', 'timed_out', '_shards', 'hits']; + +/** + * Elasticsearch response DTO + */ +export class EsResponseDto { + /** + * Number of milliseconds it + * took Elasticsearch to execute the request + */ + @IsDefined() + @IsNotEmpty() + @IsNumber() + @ApiProperty({ + description: 'took', + example: 5 + }) + took: number; + + /** + * Status of the request + * If 'true' - the request timed out before completion + */ + @IsDefined() + @IsNotEmpty() + @IsBoolean() + @ApiProperty({ + description: 'timed_out', + example: false, + }) + timed_out: boolean; + + /** + * Contains a number of Elasticsearch shards + * used for the request + */ + @IsOptional() + @IsObject() + @ApiProperty({ + description: '_shards', + example: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + } + }) + _shards: object; + + /** + * Contains returned documents and metadata + */ + @IsOptional() + @IsObject() + @ApiProperty({ + description: 'hits', + example: { + total: { + value: 3, + relation: 'eq' + }, + max_score: 1.2, + hits: [{ + _index: 'papers', + _id: '01002', + _score: 1.2, + _source: { + + }, + fields: { + + } + }], + } + }) + hits: object; +} \ No newline at end of file diff --git a/src/core/domain/dtos/index.ts b/src/core/domain/dtos/index.ts index e69de29..fd4749a 100644 --- a/src/core/domain/dtos/index.ts +++ b/src/core/domain/dtos/index.ts @@ -0,0 +1 @@ +export * from './es-response.dto' \ No newline at end of file diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index bf48d60..0304075 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,6 +1,7 @@ import { HttpService } from "@nestjs/axios"; -import { Injectable } from "@nestjs/common"; -import { map, Observable } from "rxjs"; +import { HttpException, HttpStatus, Injectable, NotAcceptableException, NotFoundException } from "@nestjs/common"; +import { map, NotFoundError, take } from "rxjs"; +import { EsResponseDto } from "src/core/domain/dtos"; /** * Search service provider @@ -9,26 +10,51 @@ import { map, Observable } from "rxjs"; export class SearchService { constructor(private readonly httpService: HttpService) {} - // Find paper by its ID - findByID(id: string): object { + /** + * Finds a paper by its own ID + * @param uuid + * @returns Elasticsearch hits or an error object + */ + async findByID(uuid: string): Promise { // Should I change 'object' to specific DTO? let es_query = { query: { query_string: { - query: 'id:' + id + query: 'id:' + uuid } } } - return this.httpService.get('http://localhost:9200/_search', { - data: es_query, - headers: {'Content-Type': 'application/json'}, - }).pipe( - map(response=>response.data) - ); + // Specify ES_PORT env! + return new Promise((resolve, reject) => { + try { + (this.httpService.get('http://localhost:9200/_search', { + data: es_query, + headers: {'Content-Type': 'application/json'}, + })) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res: any) => { + if (res.timed_out) { + reject(504); + } + + if (!res.hits.hits.length) { + reject(404); + } + + resolve(res.hits); + }); + } catch (error) { + reject(error); + } + }); } - // Find paper by context - findByContext(query_str: string): object { + /** + * Finds relevant documents by context using the given query string + * @param query_str + * @returns Elasticsearch hits or an error object + */ + findByContext(query_str: string): Promise { let es_query = { query: { query_string: { @@ -38,11 +64,27 @@ export class SearchService { } } - return this.httpService.get('http://localhost:9200/_search', { - data: es_query, - headers: {'Content-Type': 'application/json'}, - }).pipe( - map(response=>response.data) - ); + return new Promise((resolve, reject) => { + try { + (this.httpService.get('http://localhost:9200/_search', { + data: es_query, + headers: {'Content-Type': 'application/json'}, + })) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res: any) => { + if (res.timed_out) { + reject(504); + } + + if (!res.hits.hits.length) { + reject(404); + } + + resolve(res.hits); + }); + } catch (error) { + reject(error); + } + }); } } \ No newline at end of file diff --git a/src/infrastructure/modules/app.module.ts b/src/infrastructure/modules/app.module.ts index 90c3573..d70c4cd 100644 --- a/src/infrastructure/modules/app.module.ts +++ b/src/infrastructure/modules/app.module.ts @@ -7,7 +7,7 @@ import { LoggerInterceptor } from '../../core/interceptors' import * as modules from '../../core/modules' import { CommonModule } from './common/common.module'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; -import { PapersController } from 'src/application'; +import { PapersController } from 'src/application/controller/papers.controller'; import { SearchModule } from './search.module'; /** diff --git a/src/infrastructure/modules/search.module.ts b/src/infrastructure/modules/search.module.ts index a7c9ecf..027d34f 100644 --- a/src/infrastructure/modules/search.module.ts +++ b/src/infrastructure/modules/search.module.ts @@ -1,6 +1,6 @@ import { HttpModule } from "@nestjs/axios"; import { Module } from "@nestjs/common"; -import { SearchService } from "src/core/services/common/search.service"; +import { SearchService } from "../../core/services/common/search.service"; /** * search module diff --git a/src/test/papers.endpoint.spec.ts b/src/test/papers.endpoint.spec.ts new file mode 100644 index 0000000..18c7da9 --- /dev/null +++ b/src/test/papers.endpoint.spec.ts @@ -0,0 +1,51 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import request from 'supertest' +import { INestApplication } from "@nestjs/common"; +import { AppModule } from "../infrastructure/modules"; + +describe('PapersController', () => { + let app: INestApplication; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + }); + + /** + * Getting paper by its ID + */ + it('Should be 404', () => { + let uuid = 'eeeb2d01-8315-454e-b33f-3d6caa25db43'; + return request(app.getHttpServer()) + .get('/papers/' + uuid) + .expect(404); + }); + + it('Should be 200', () => { + let uuid = 'eeeb2d01-8315-454e-b33f-3d6caa25db42'; + return request(app.getHttpServer()) + .get('/papers/' + uuid) + .expect(200); + }); + + /** + * Getting paper by the given context + */ + it('Should be 404', () => { + let q_str = 'Explosion'; // Non-existing word in the test-paper + return request(app.getHttpServer()) + .get('/papers/search?query=' + q_str) + .expect(404); + }); + + it('Should be 200', () => { + let q_str = 'Docker'; // Existing word in the test-paper + return request(app.getHttpServer()) + .get('/papers/search?query=' + q_str) + .expect(200); + }); +}); \ No newline at end of file diff --git a/test/.gitkeep b/test/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/search.module.spec.ts b/test/search.module.spec.ts deleted file mode 100644 index c23f829..0000000 --- a/test/search.module.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing"; -import { PapersController } from "../src/application/controller/papers.controller"; - -describe('PapersController', () => { - let controller: PapersController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [PapersController], - }).compile(); - - controller = module.get(PapersController); - }); - - it('Should be defined', () => { - expect(controller).toBeDefined(); - }); -}); \ No newline at end of file